├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── IDEAS.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── alias-cloudformation-template.json ├── aliasRestructureStack.js ├── collectUserResources.js ├── configureAliasStack.js ├── createAliasStack.js ├── deferredOutputs.js ├── listAliases.js ├── logs.js ├── removeAlias.js ├── stackInformation.js ├── stackops │ ├── apiGateway.js │ ├── cwEvents.js │ ├── events.js │ ├── functions.js │ ├── init.js │ ├── lambdaRole.js │ ├── snsEvents.js │ └── userResources.js ├── updateAliasStack.js ├── updateFunctionAlias.js ├── uploadAliasArtifacts.js ├── utils.js └── validate.js ├── package-lock.json ├── package.json └── test ├── aliasRestructureStack.test.js ├── configureAliasStack.test.js ├── createAliasStack.test.js ├── data ├── alias-stack-1.json ├── alias-stack-2.json ├── auth-stack-2.json ├── auth-stack.json ├── sls-stack-1.json ├── sls-stack-2.json ├── sls-stack-3.json └── sns-stack.json ├── index.test.js ├── logs.test.js ├── removeAlias.test.js ├── stackops ├── apiGateway.test.js ├── init.test.js ├── lambdaRole.test.js └── snsEvents.test.js ├── updateAliasStack.test.js ├── uploadAliasArtifacts.test.js ├── utils.test.js └── validate.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | 9 | [*.{json,yml}] 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | es6: true 4 | node: true 5 | mocha: true 6 | 7 | extends: 8 | - eslint:recommended 9 | - plugin:lodash/recommended 10 | - plugin:import/errors 11 | - plugin:import/warnings 12 | 13 | plugins: 14 | - promise 15 | - lodash 16 | - import 17 | 18 | rules: 19 | indent: 20 | - error 21 | - tab 22 | - 23 | MemberExpression: off 24 | linebreak-style: 25 | - error 26 | - unix 27 | semi: 28 | - error 29 | - always 30 | promise/always-return: error 31 | promise/no-return-wrap: error 32 | promise/param-names: error 33 | promise/catch-or-return: error 34 | promise/no-native: off 35 | promise/no-nesting: off 36 | promise/no-promise-in-callback: warn 37 | promise/no-callback-in-promise: warn 38 | promise/avoid-new: warn 39 | import/no-named-as-default: off 40 | lodash/import-scope: off 41 | lodash/preferred-alias: off 42 | lodash/prop-shorthand: off 43 | lodash/prefer-lodash-method: 44 | - error 45 | - 46 | ignoreObjects: 47 | - BbPromise 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | # Jetbrains IDEs 4 | .idea/ 5 | 6 | # Platform specific 7 | .DS_Store 8 | 9 | # Coverage directories used by tools like istanbul 10 | .nyc_output/ 11 | coverage/ 12 | 13 | # Code style tools 14 | .eslintcache 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | matrix: 4 | include: 5 | - node_js: '8' 6 | 7 | sudo: false 8 | 9 | install: 10 | - travis_retry npm install 11 | 12 | script: 13 | - npm run eslint 14 | - npm test 15 | 16 | after_success: 17 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 18 | -------------------------------------------------------------------------------- /IDEAS.md: -------------------------------------------------------------------------------- 1 | Alias plugin ideas 2 | ================== 3 | 4 | configuration 5 | ------------- 6 | * Support for alias independent resources and references. 7 | * Support for alias based log streams or (as in 0.5) service based log streams. 8 | * Allow functions to be tagged as "instance per alias", for functions that should be deployed 9 | as different functions for each alias. Examples are functions that are references from other 10 | AWS services that do not allow refereincing aliases. Optimally the system should change the 11 | physical stack layout accordingly if this property is changed and keep the deployment history. 12 | 13 | 14 | deploy 15 | ------ 16 | Deploy without a default stage will only create the stage independent stack containing 17 | the empty function definitions, log streams and the (new) stage independent resources. 18 | The default alias name is the stage name. That ensures the system also works in case someone 19 | does not deploy aliases at all. 20 | 21 | deploy stage 22 | ------------ 23 | Deploys/updates a stage stack (and the service stack if not already deployed). If a new stage name 24 | is given, the stage will be implicitly created (including the stage dependent resources). 25 | 26 | If a stage is deployed to a different region there are multiple ways to handle this: 27 | * Completely independent 28 | A region could be seen as completely independent environment, i.e. the service stack will be deployed 29 | to the region too. 30 | * As service stage only 31 | Only the stage stack will be deployed to the other region and the current service stack region 32 | would be the "home" of the service. If the service is removed, it will also remove the deployed stage 33 | in all other regions (the deployed stage in the other region will reference the service stack in the 34 | home region). 35 | 36 | Personally I'd prefer the "service stage only" approach - the independent approach could be used for 37 | cross account deployments. 38 | 39 | rollback stage 40 | -------------- 41 | Rollback a stage to the previously deployed version 42 | 43 | reset stage 44 | ----------- 45 | Kind of like the "git reset" command. This should allow you to reset a stage version to the same 46 | version as another stage. Would be great for developers that work on their own stages and want to 47 | start with a fresh deployment. 48 | 49 | remove stage 50 | ------------ 51 | Stage removal can be done by just deleting the stage dependent stack. All aliases and possibly 52 | orphaned function versions should be removed automatically. 53 | 54 | clone stage 55 | ----------- 56 | With stage dependent stacks it should be quite easy to provide a _close stage_ functionality 57 | that will clone an existing stage and set the aliases automatically to the same versions of the 58 | origin stage. 59 | 60 | 61 | Open 62 | ---- 63 | Review technical documentation how to apply the same semantics to APIG (staged deployment via CF). 64 | 65 | 66 | Implementation specification 67 | ---------------------------- 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2016,2017 Frank Schmid 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Serverless AWS alias plugin 5 | */ 6 | 7 | const BbPromise = require('bluebird') 8 | , _ = require('lodash') 9 | , Path = require('path') 10 | , validate = require('./lib/validate') 11 | , configureAliasStack = require('./lib/configureAliasStack') 12 | , createAliasStack = require('./lib/createAliasStack') 13 | , updateAliasStack = require('./lib/updateAliasStack') 14 | , aliasRestructureStack = require('./lib/aliasRestructureStack') 15 | , stackInformation = require('./lib/stackInformation') 16 | , listAliases = require('./lib/listAliases') 17 | , removeAlias = require('./lib/removeAlias') 18 | , logs = require('./lib/logs') 19 | , collectUserResources = require('./lib/collectUserResources') 20 | , uploadAliasArtifacts = require('./lib/uploadAliasArtifacts') 21 | , updateFunctionAlias = require('./lib/updateFunctionAlias') 22 | , deferredOutputs = require('./lib/deferredOutputs'); 23 | 24 | class AwsAlias { 25 | 26 | constructor(serverless, options) { 27 | this._serverless = serverless; 28 | this._options = options || {}; 29 | this._provider = this._serverless.getProvider('aws'); 30 | 31 | /** 32 | * Set preliminary stage and alias. This is needed to enable the injection 33 | * of the stage into the system variables. The values are overwritten by 34 | * validate with their actually evaluated and substituted values. 35 | */ 36 | this._stage = this._provider.getStage(); 37 | this._alias = this._options.alias || this._stage; 38 | this._serverless.service.provider.alias = this._alias; 39 | 40 | /** 41 | * Load stack helpers from Serverless installation. 42 | */ 43 | const monitorStack = require( 44 | Path.join(this._serverless.config.serverlessPath, 45 | 'plugins', 46 | 'aws', 47 | 'lib', 48 | 'monitorStack') 49 | ); 50 | const setBucketName = require( 51 | Path.join(this._serverless.config.serverlessPath, 52 | 'plugins', 53 | 'aws', 54 | 'lib', 55 | 'setBucketName') 56 | ); 57 | 58 | _.assign( 59 | this, 60 | validate, 61 | collectUserResources, 62 | configureAliasStack, 63 | createAliasStack, 64 | updateAliasStack, 65 | listAliases, 66 | logs, 67 | removeAlias, 68 | aliasRestructureStack, 69 | stackInformation, 70 | uploadAliasArtifacts, 71 | updateFunctionAlias, 72 | setBucketName, 73 | monitorStack, 74 | deferredOutputs 75 | ); 76 | 77 | this._commands = { 78 | alias: { 79 | commands: { 80 | remove: { 81 | usage: 'Remove a deployed alias', 82 | lifecycleEvents: [ 83 | 'remove' 84 | ], 85 | options: { 86 | alias: { 87 | usage: 'Name of the alias', 88 | shortcut: 'a', 89 | required: true 90 | }, 91 | verbose: { 92 | usage: 'Enable verbose output', 93 | shortcut: 'v', 94 | required: false 95 | } 96 | } 97 | } 98 | } 99 | } 100 | }; 101 | 102 | this._hooks = { 103 | 'before:package:initialize': () => BbPromise.bind(this) 104 | .then(this.validate), 105 | 106 | 'before:aws:package:finalize:mergeCustomProviderResources': () => BbPromise.bind(this) 107 | .then(this.collectUserResources), 108 | 109 | 'before:deploy:deploy': () => BbPromise.bind(this) 110 | .then(this.validate) 111 | .then(this.configureAliasStack), 112 | 113 | 'before:aws:deploy:deploy:createStack': () => BbPromise.bind(this) 114 | .then(this.aliasStackLoadCurrentCFStackAndDependencies) 115 | .spread(this.aliasRestructureStack), 116 | 117 | 'after:aws:deploy:deploy:createStack': () => BbPromise.bind(this) 118 | .then(this.createAliasStack), 119 | 120 | 'after:aws:deploy:deploy:uploadArtifacts': () => BbPromise.bind(this) 121 | .then(() => BbPromise.resolve()), 122 | 123 | 'after:aws:deploy:deploy:updateStack': () => BbPromise.bind(this) 124 | .then(this.setBucketName) 125 | .then(this.uploadAliasArtifacts) 126 | .then(this.updateAliasStack), 127 | 128 | 'before:deploy:function:initialize': () => BbPromise.bind(this) 129 | .then(this.validate) 130 | .then(() => { 131 | // Force forced deploy 132 | if (!this._options.force) { 133 | return BbPromise.reject(new this.serverless.classes.Error("You must deploy single functions using --force with the alias plugin.")); 134 | } 135 | return BbPromise.resolve(); 136 | }), 137 | 138 | 'after:deploy:function:deploy': () => BbPromise.bind(this) 139 | .then(this.updateFunctionAlias), 140 | 141 | 'after:info:info': () => BbPromise.bind(this) 142 | .then(this.validate) 143 | .then(this.listAliases), 144 | 145 | 'before:remove:remove': () => { 146 | if (!this._validated) { 147 | return BbPromise.reject(new this._serverless.classes.Error(`Use "serverless alias remove --alias=${this._stage}" to remove the service.`)); 148 | } 149 | return BbPromise.resolve(); 150 | }, 151 | 152 | // Override the logs command - must be, because the $LATEST filter 153 | // in the original logs command is not easy to change without hacks. 154 | 'logs:logs': () => BbPromise.bind(this) 155 | .then(this.validate) 156 | .then(this.logsValidate) 157 | .then(this.logsGetLogStreams) 158 | .then(this.functionLogsShowLogs), 159 | 160 | 'logs:api:logs': () => BbPromise.bind(this) 161 | .then(this.validate) 162 | .then(this.apiLogsValidate) 163 | .then(this.apiLogsGetLogStreams) 164 | .then(this.apiLogsShowLogs), 165 | 166 | 'alias:remove:remove': () => BbPromise.bind(this) 167 | .then(this.validate) 168 | .then(this.aliasStackLoadCurrentCFStackAndDependencies) 169 | .spread(this.removeAlias) 170 | }; 171 | 172 | // Patch hooks to override our event replacements 173 | const pluginManager = this.serverless.pluginManager; 174 | const logHooks = pluginManager.hooks['logs:logs']; 175 | _.pullAllWith(logHooks, [ 'AwsLogs' ], (a, b) => a.pluginName === b); 176 | 177 | // Extend the logs command if available 178 | try { 179 | const logCommand = pluginManager.getCommand([ 'logs' ]); 180 | logCommand.options.alias = { 181 | usage: 'Alias' 182 | }; 183 | logCommand.options.version = { 184 | usage: 'Logs a specific version of the function' 185 | }; 186 | logCommand.commands = _.assign({}, logCommand.commands, { 187 | api: { 188 | usage: 'Output the logs of a deployed APIG stage (alias)', 189 | lifecycleEvents: [ 190 | 'logs', 191 | ], 192 | options: { 193 | alias: { 194 | usage: 'Alias' 195 | }, 196 | stage: { 197 | usage: 'Stage of the service', 198 | shortcut: 's', 199 | }, 200 | region: { 201 | usage: 'Region of the service', 202 | shortcut: 'r', 203 | }, 204 | tail: { 205 | usage: 'Tail the log output', 206 | shortcut: 't', 207 | }, 208 | startTime: { 209 | usage: 'Logs before this time will not be displayed', 210 | }, 211 | filter: { 212 | usage: 'A filter pattern', 213 | }, 214 | interval: { 215 | usage: 'Tail polling interval in milliseconds. Default: `1000`', 216 | shortcut: 'i', 217 | }, 218 | }, 219 | key: 'logs:api', 220 | pluginName: 'Logs', 221 | commands: {}, 222 | } 223 | }); 224 | } catch (e) { 225 | // Do nothing 226 | } 227 | } 228 | 229 | /** 230 | * Expose the supported commands as read-only property. 231 | */ 232 | get commands() { 233 | return this._commands; 234 | } 235 | 236 | /** 237 | * Expose the supported hooks as read-only property. 238 | */ 239 | get hooks() { 240 | return this._hooks; 241 | } 242 | 243 | /** 244 | * Expose the options as read-only property. 245 | */ 246 | get options() { 247 | return this._options; 248 | } 249 | 250 | /** 251 | * Expose the supported provider as read-only property. 252 | */ 253 | get provider() { 254 | return this._provider; 255 | } 256 | 257 | /** 258 | * Expose the serverless object as read-only property. 259 | */ 260 | get serverless() { 261 | return this._serverless; 262 | } 263 | 264 | /** 265 | * Expose the stack name as read-only property. 266 | */ 267 | get stackName() { 268 | return this._stackName; 269 | } 270 | 271 | _cleanup() { 272 | this._serverless.cli.log('Cleanup !!!!'); 273 | } 274 | 275 | } 276 | 277 | module.exports = AwsAlias; 278 | -------------------------------------------------------------------------------- /lib/alias-cloudformation-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Alias stack template", 4 | "Resources": { 5 | }, 6 | "Outputs": { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/aliasRestructureStack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Rebuild the stack structure. 4 | * =========================== 5 | * This enables us to deploy different function/resource sets per alias, e.g. 6 | * if a developer wants to deploy his very own branch as an alias. 7 | * We also have to retrieve the currently deployed stack template to 8 | * check for functions that might have been deleted in all other alias 9 | * stacks, or ones that have been added in the current alias stack. 10 | */ 11 | 12 | const BbPromise = require('bluebird'); 13 | const _ = require('lodash'); 14 | 15 | const init = require('./stackops/init'); 16 | const functions = require('./stackops/functions'); 17 | const apiGateway = require('./stackops/apiGateway'); 18 | const userResources = require('./stackops/userResources'); 19 | const lambdaRole = require('./stackops/lambdaRole'); 20 | const events = require('./stackops/events'); 21 | const cwEvents = require('./stackops/cwEvents'); 22 | const snsEvents = require('./stackops/snsEvents'); 23 | 24 | module.exports = { 25 | 26 | aliasInit(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 27 | return init.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate); 28 | }, 29 | 30 | aliasHandleFunctions(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 31 | return functions.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate); 32 | }, 33 | 34 | aliasHandleApiGateway(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 35 | return apiGateway.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate); 36 | }, 37 | 38 | aliasHandleUserResources(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 39 | return userResources.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate); 40 | }, 41 | 42 | aliasHandleLambdaRole(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 43 | return lambdaRole.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate); 44 | }, 45 | 46 | aliasHandleEvents(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 47 | return events.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate); 48 | }, 49 | 50 | aliasHandleCWEvents(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 51 | return cwEvents.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate); 52 | }, 53 | 54 | aliasHandleSNSEvents(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 55 | return snsEvents.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate); 56 | }, 57 | 58 | aliasFinalize(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 59 | const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate; 60 | const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate; 61 | 62 | aliasStack.Outputs.AliasFlags.Value = JSON.stringify(aliasStack.Outputs.AliasFlags.Value); 63 | 64 | // Check for missing dependencies and integrate them too 65 | _.forEach(_.filter(stageStack.Resources, resource => !_.isEmpty(resource.DependsOn)), parent => { 66 | _.forEach(parent.DependsOn, child => { 67 | if (!_.has(stageStack.Resources, child) && _.has(currentTemplate.Resources, child)) { 68 | stageStack.Resources[child] = currentTemplate.Resources[child]; 69 | } 70 | }); 71 | }); 72 | 73 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 74 | }, 75 | 76 | addMasterAliasName(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 77 | const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate; 78 | if (stageStack && !stageStack.Outputs.MasterAliasName) { 79 | const masterAlias = this._masterAlias || currentTemplate.Outputs.MasterAliasName.Value; 80 | stageStack.Outputs.MasterAliasName = { 81 | Description: 'Master Alias name (serverless-aws-alias plugin)', 82 | Value: masterAlias, 83 | Export: { 84 | Name: `${this._provider.naming.getStackName()}-MasterAliasName` 85 | } 86 | }; 87 | } 88 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 89 | }, 90 | 91 | aliasRestructureStack(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 92 | this._serverless.cli.log('Preparing alias ...'); 93 | 94 | if (_.isEmpty(aliasStackTemplates) && this._masterAlias !== this._alias) { 95 | throw new this._serverless.classes.Error(new Error('You have to deploy the master alias at least once with "serverless deploy [--masterAlias]"')); 96 | } 97 | 98 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]).bind(this) 99 | .spread(this.addMasterAliasName) 100 | .spread(this.aliasInit) 101 | .spread(this.aliasHandleUserResources) 102 | .spread(this.aliasHandleLambdaRole) 103 | .spread(this.aliasHandleFunctions) 104 | .spread(this.aliasHandleApiGateway) 105 | .spread(this.aliasHandleEvents) 106 | .spread(this.aliasHandleCWEvents) 107 | .spread(this.aliasHandleSNSEvents) 108 | .spread(this.aliasFinalize) 109 | .then(() => BbPromise.resolve()); 110 | } 111 | 112 | }; 113 | -------------------------------------------------------------------------------- /lib/collectUserResources.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Persist the user resources. 4 | * This is now necessary as the package command merges them already. 5 | */ 6 | 7 | const BbPromise = require('bluebird'); 8 | const _ = require('lodash'); 9 | 10 | module.exports = { 11 | collectUserResources() { 12 | this._serverless.service.provider.aliasUserResources = 13 | _.cloneDeep( 14 | _.get(this._serverless.service, 'resources', { Resources: {}, Outputs: {} })); 15 | 16 | return BbPromise.resolve(); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /lib/configureAliasStack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Create the alias stack for the service. 4 | * 5 | * The alias stack contains the function definition and exposes the functions 6 | * as CF output variables that are referenced in the stage dependent CF stacks. 7 | */ 8 | 9 | const BbPromise = require('bluebird'); 10 | const _ = require('lodash'); 11 | const path = require('path'); 12 | 13 | module.exports = { 14 | 15 | configureAliasStack() { 16 | 17 | const compiledTemplate = this._serverless.service.provider.compiledCloudFormationTemplate; 18 | 19 | // Export an Output variable that will be referenced by the alias stacks 20 | // so that we are able to list all dependent alias deployments easily. 21 | compiledTemplate.Outputs.ServerlessAliasReference = { 22 | Description: 'Alias stack reference', 23 | Value: 'REFERENCE', 24 | Export: { 25 | Name: `${this._provider.naming.getStackName()}-ServerlessAliasReference` 26 | } 27 | }; 28 | 29 | this._aliasStackName = `${this._provider.naming.getStackName()}-${this._alias}`; 30 | 31 | /** 32 | * Prepare the alias stack template. 33 | */ 34 | this._serverless.service.provider 35 | .compiledCloudFormationAliasTemplate = this._serverless.utils.readFileSync( 36 | path.join(__dirname, 'alias-cloudformation-template.json') 37 | ); 38 | 39 | const aliasTemplate = this._serverless.service.provider.compiledCloudFormationAliasTemplate; 40 | 41 | /** 42 | * Set a proper stack decription 43 | */ 44 | aliasTemplate.Description = `Alias stack for ${this._stackName} (${this._alias})`; 45 | 46 | /** 47 | * Add the alias name as output variable to the stack. 48 | */ 49 | aliasTemplate.Outputs.ServerlessAliasName = { 50 | Description: 'Alias the stack represents.', 51 | Value: `${this._alias}` 52 | }; 53 | 54 | /** 55 | * Create a log group to capture alias modification history. 56 | */ 57 | aliasTemplate.Resources.ServerlessAliasLogGroup = { 58 | Type: 'AWS::Logs::LogGroup', 59 | Properties: { 60 | LogGroupName: `/serverless/${this._provider.naming.getStackName()}-${this._alias}`, 61 | RetentionInDays: 7 62 | } 63 | }; 64 | aliasTemplate.Outputs.ServerlessAliasLogGroup = { 65 | Description: 'Log group for alias.', 66 | Value: { Ref: 'ServerlessAliasLogGroup' }, 67 | Export: { 68 | Name: `${this._aliasStackName}-LogGroup` 69 | } 70 | }; 71 | 72 | this._serverless.service.provider.compiledCloudFormationAliasCreateTemplate = _.cloneDeep(aliasTemplate); 73 | 74 | return BbPromise.resolve(); 75 | } 76 | 77 | }; 78 | -------------------------------------------------------------------------------- /lib/createAliasStack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Create the alias stack for the service. 4 | * 5 | * The alias stack contains the function definition and exposes the functions 6 | * as CF output variables that are referenced in the stage dependent CF stacks. 7 | */ 8 | 9 | const BbPromise = require('bluebird'); 10 | const _ = require('lodash'); 11 | const path = require('path'); 12 | 13 | module.exports = { 14 | 15 | createAlias() { 16 | 17 | this._serverless.cli.log(`Creating Alias Stack '${this._alias}' ...`); 18 | const stackName = `${this._provider.naming.getStackName()}-${this._alias}`; 19 | let stackTags = { STAGE: this._options.stage, ALIAS: this._alias }; 20 | 21 | // Merge additional stack tags 22 | if (_.isObject(this._serverless.service.provider.stackTags)) { 23 | stackTags = _.extend(stackTags, this._serverless.service.provider.stackTags); 24 | } 25 | 26 | const params = { 27 | StackName: stackName, 28 | OnFailure: 'DELETE', 29 | Capabilities: [ 30 | 'CAPABILITY_IAM', 31 | 'CAPABILITY_NAMED_IAM', 32 | ], 33 | Parameters: [], 34 | TemplateBody: JSON.stringify(this._serverless.service.provider.compiledCloudFormationAliasCreateTemplate), 35 | Tags: _.map(_.keys(stackTags), key => ({ Key: key, Value: stackTags[key] })) 36 | }; 37 | 38 | return this._provider.request( 39 | 'CloudFormation', 40 | 'createStack', 41 | params, 42 | this._options.stage, 43 | this._options.region 44 | ).then(cfData => this.monitorStack('create', cfData)); 45 | 46 | }, 47 | 48 | createAliasStack() { 49 | 50 | this._aliasStackName = `${this._provider.naming.getStackName()}-${this._alias}`; 51 | if (/^[^a-zA-Z].+|.*[^a-zA-Z0-9-].*/.test(this._aliasStackName) || this._aliasStackName.length > 128) { 52 | const errorMessage = [ 53 | `The stack alias name "${this._aliasStackName}" is not valid. `, 54 | 'A service name should only contain alphanumeric', 55 | ' (case sensitive) and hyphens. It should start', 56 | ' with an alphabetic character and shouldn\'t', 57 | ' exceed 128 characters.', 58 | ].join(''); 59 | throw new this._serverless.classes.Error(errorMessage); 60 | } 61 | 62 | return BbPromise.bind(this) 63 | // always write the template to disk, whether we are deploying or not 64 | .then(this.writeAliasTemplateToDisk) 65 | .then(this.checkAliasStack); 66 | }, 67 | 68 | checkAliasStack() { 69 | 70 | if (this._options.noDeploy) { 71 | return BbPromise.resolve(); 72 | } 73 | 74 | return this._provider.request('CloudFormation', 75 | 'describeStackResources', 76 | { StackName: this._aliasStackName }, 77 | this._options.stage, 78 | this._options.region) 79 | .then(() => BbPromise.resolve('alreadyCreated')) 80 | .catch(e => { 81 | if (_.includes(e.message, 'does not exist')) { 82 | if (this._serverless.service.provider.deploymentBucket) { 83 | this._createLater = true; 84 | return BbPromise.resolve(); 85 | } 86 | 87 | return BbPromise.bind(this) 88 | .then(this.createAlias); 89 | } 90 | 91 | return BbPromise.reject(e); 92 | }); 93 | 94 | }, 95 | 96 | writeAliasTemplateToDisk() { 97 | 98 | if (this._serverless.service.provider.deploymentBucket) { 99 | return BbPromise.resolve(); 100 | } 101 | 102 | const cfTemplateFilePath = path.join(this._serverless.config.servicePath, 103 | '.serverless', 'cloudformation-template-create-alias-stack.json'); 104 | 105 | this._serverless.utils.writeFileSync(cfTemplateFilePath, 106 | this._serverless.service.provider.compiledCloudFormationAliasCreateTemplate); 107 | 108 | return BbPromise.resolve(); 109 | } 110 | 111 | }; 112 | -------------------------------------------------------------------------------- /lib/deferredOutputs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Handle deferred output resolution. 5 | * Some references to outputs of the base stack cannot be done 6 | * by Fn::ImportValue because they will change from deployment to deployment. 7 | * So we resolve them after the base stack has been deployed and set their 8 | * values accordingly. 9 | */ 10 | 11 | const _ = require('lodash'); 12 | const BbPromise = require('bluebird'); 13 | 14 | const deferredOutputs = {}; 15 | 16 | module.exports = { 17 | 18 | /** 19 | * Register a deferred output 20 | * @param {string} sourceOutput 21 | * @param {Object} targetObject 22 | * @param {string} targetPropertyName 23 | */ 24 | addDeferredOutput(sourceOutput, targetObject, targetPropertyName) { 25 | this.options.verbose && this.serverless.cli.log(`Register deferred output ${sourceOutput} -> ${targetPropertyName}`); 26 | 27 | deferredOutputs[sourceOutput] = deferredOutputs[sourceOutput] || []; 28 | deferredOutputs[sourceOutput].push({ 29 | target: targetObject, 30 | property: targetPropertyName 31 | }); 32 | }, 33 | 34 | resolveDeferredOutputs() { 35 | this.options.verbose && this.serverless.cli.log('Resolving deferred outputs'); 36 | 37 | if (_.isEmpty(deferredOutputs)) { 38 | return BbPromise.resolve(); 39 | } 40 | 41 | return this.aliasGetExports() 42 | .then(cfExports => { 43 | _.forOwn(deferredOutputs, (references, output) => { 44 | if (_.has(cfExports, output)) { 45 | const value = cfExports[output]; 46 | this.options.verbose && this.serverless.cli.log(` ${output} -> ${value}`); 47 | _.forEach(references, reference => { 48 | _.set(reference.target, reference.property, value); 49 | }); 50 | } 51 | else { 52 | this.serverless.cli.log(`ERROR: Output ${output} not found.`); 53 | } 54 | }); 55 | return null; 56 | }); 57 | } 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /lib/listAliases.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * List all deployed aliases 4 | */ 5 | 6 | const BbPromise = require('bluebird'); 7 | const _ = require('lodash'); 8 | const chalk = require('chalk'); 9 | 10 | /* eslint no-console: "off" */ 11 | 12 | module.exports = { 13 | 14 | listDescribeApiStage(apiId, stageName) { 15 | 16 | if (!apiId) { 17 | return BbPromise.resolve(null); 18 | } 19 | 20 | return this._provider.request('APIGateway', 21 | 'getStage', 22 | { 23 | restApiId: apiId, 24 | stageName: stageName 25 | }, 26 | this._options.stage, 27 | this._options.region) 28 | .then(stage => { 29 | return this._provider.request('APIGateway', 30 | 'getDeployment', 31 | { 32 | restApiId: apiId, 33 | deploymentId: stage.deploymentId 34 | }, 35 | this._options.stage, 36 | this._options.region); 37 | }) 38 | .catch(err => { 39 | if (/^Invalid stage/.test(err.message)) { 40 | return BbPromise.resolve(null); 41 | } 42 | return BbPromise.reject(err); 43 | }); 44 | }, 45 | 46 | listGetApiId(stackName) { 47 | 48 | return this._provider.request('CloudFormation', 49 | 'describeStackResource', 50 | { 51 | LogicalResourceId: 'ApiGatewayRestApi', 52 | StackName: stackName 53 | }, 54 | this._options.stage, 55 | this._options.region) 56 | .then(cfData => cfData.StackResourceDetail.PhysicalResourceId) 57 | .catch(() => BbPromise.resolve(null)); 58 | }, 59 | 60 | listAliases() { 61 | 62 | console.log(chalk.yellow('aliases:')); 63 | 64 | return BbPromise.join( 65 | BbPromise.bind(this).then(() => { 66 | return this.aliasStackGetAliasStackNames() 67 | .mapSeries(stack => this.aliasStackLoadTemplate(stack)); 68 | }), 69 | this.listGetApiId(this._provider.naming.getStackName()) 70 | ) 71 | .spread((aliasStackTemplates, apiId) => { 72 | return BbPromise.mapSeries(aliasStackTemplates, aliasTemplate => { 73 | 74 | const aliasName = _.get(aliasTemplate, 'Outputs.ServerlessAliasName.Value'); 75 | if (aliasName) { 76 | console.log(chalk.white(` ${aliasName}`)); 77 | 78 | if (this._options.verbose) { 79 | return BbPromise.join( 80 | this.aliasGetAliasFunctionVersions(aliasName), 81 | this.listDescribeApiStage(apiId, aliasName) 82 | ) 83 | .spread((versions /*, apiStage */) => { 84 | console.log(chalk.white(' Functions:')); 85 | _.forEach(versions, version => { 86 | console.log(chalk.yellow(` ${version.functionName} -> ${version.functionVersion}`)); 87 | 88 | // Print deployed endpoints for the function 89 | // FIXME: Check why APIG getStage and getDeployment do not return the stage API layout. 90 | 91 | }); 92 | 93 | return BbPromise.resolve(); 94 | }); 95 | } 96 | } 97 | }); 98 | }); 99 | } 100 | 101 | }; 102 | -------------------------------------------------------------------------------- /lib/logs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Log management. 4 | */ 5 | 6 | const BbPromise = require('bluebird'); 7 | const _ = require('lodash'); 8 | const chalk = require('chalk'); 9 | const moment = require('moment'); 10 | const os = require('os'); 11 | 12 | function getApiLogGroupName(apiId, alias) { 13 | return `API-Gateway-Execution-Logs_${apiId}/${alias}`; 14 | } 15 | 16 | module.exports = { 17 | 18 | logsValidate() { 19 | this._lambdaName = this._serverless.service.getFunction(this.options.function).name; 20 | this._options.logGroupName = this._provider.naming.getLogGroupName(this._lambdaName); 21 | this._options.interval = this._options.interval || 1000; 22 | 23 | return BbPromise.resolve(); 24 | }, 25 | 26 | apiLogsValidate() { 27 | if (this.options.function) { 28 | return BbPromise.reject(new this.serverless.classes.Error('--function is not supported for API logs.')); 29 | } 30 | 31 | // Retrieve APIG id 32 | return this.aliasStacksDescribeResource('ApiGatewayRestApi') 33 | .then(resources => { 34 | if (_.isEmpty(resources.StackResources)) { 35 | return BbPromise.reject(new this.serverless.classes.Error('service does not contain any API')); 36 | } 37 | 38 | const apiResource = _.first(resources.StackResources); 39 | const apiId = apiResource.PhysicalResourceId; 40 | this._apiLogsLogGroup = getApiLogGroupName(apiId, this._alias); 41 | this._options.interval = this._options.interval || 1000; 42 | 43 | this.options.verbose && this.serverless.cli.log(`API id: ${apiId}`); 44 | this.options.verbose && this.serverless.cli.log(`Log group: ${this._apiLogsLogGroup}`); 45 | 46 | return BbPromise.resolve(); 47 | }); 48 | }, 49 | 50 | logsGetLogStreams() { 51 | const params = { 52 | logGroupName: this._options.logGroupName, 53 | descending: true, 54 | limit: 50, 55 | orderBy: 'LastEventTime', 56 | }; 57 | 58 | let aliasGetAliasFunctionVersion; 59 | // Check if --version is specified 60 | if (this._options.version) { 61 | aliasGetAliasFunctionVersion = BbPromise.resolve(this._options.version); 62 | } else { 63 | aliasGetAliasFunctionVersion = this.aliasGetAliasLatestFunctionVersionByFunctionName(this._alias, this._lambdaName); 64 | } 65 | // Get currently deployed function version for the alias to 66 | // setup the stream filter correctly 67 | return aliasGetAliasFunctionVersion 68 | .then(version => { 69 | if (!version) { 70 | return BbPromise.reject(new this.serverless.classes.Error('Function alias not found.')); 71 | } 72 | 73 | return this.provider 74 | .request('CloudWatchLogs', 75 | 'describeLogStreams', 76 | params, 77 | this.options.stage, 78 | this.options.region) 79 | .then(reply => { 80 | if (!reply || _.isEmpty(reply.logStreams)) { 81 | throw new this.serverless.classes 82 | .Error('No existing streams for the function alias'); 83 | } 84 | const logStreamNames = _.map( 85 | _.filter(reply.logStreams, stream => _.includes(stream.logStreamName, `[${version}]`)), 86 | stream => stream.logStreamName); 87 | 88 | if (_.isEmpty(logStreamNames)) { 89 | return BbPromise.reject(new this.serverless.classes.Error('No existing streams for this function version. If you want to view logs of a specific function version, please use --version')); 90 | } 91 | return logStreamNames; 92 | }); 93 | }); 94 | }, 95 | 96 | apiLogsGetLogStreams() { 97 | const params = { 98 | logGroupName: this._apiLogsLogGroup, 99 | descending: true, 100 | limit: 50, 101 | orderBy: 'LastEventTime', 102 | }; 103 | 104 | return this.provider.request( 105 | 'CloudWatchLogs', 106 | 'describeLogStreams', 107 | params, 108 | this.options.stage, 109 | this.options.region 110 | ) 111 | .then(reply => { 112 | if (!reply || _.isEmpty(reply.logStreams)) { 113 | return BbPromise.reject(new this.serverless.classes.Error('No logs exist for the API')); 114 | } 115 | 116 | return _.map(reply.logStreams, stream => stream.logStreamName); 117 | }); 118 | 119 | }, 120 | 121 | apiLogsShowLogs(logStreamNames) { 122 | const formatApiLogEvent = event => { 123 | const dateFormat = 'YYYY-MM-DD HH:mm:ss.SSS (Z)'; 124 | const timestamp = chalk.green(moment(event.timestamp).format(dateFormat)); 125 | 126 | const parsedMessage = /\((.*?)\) .*/.exec(event.message); 127 | const header = `${timestamp} ${chalk.yellow(parsedMessage[1])}${os.EOL}`; 128 | const message = chalk.gray(_.replace(event.message, /\(.*?\) /, '')); 129 | return `${header}${message}${os.EOL}`; 130 | }; 131 | 132 | return this.logsShowLogs(logStreamNames, formatApiLogEvent, this.apiLogsGetLogStreams.bind(this)); 133 | }, 134 | 135 | functionLogsShowLogs(logStreamNames) { 136 | const formatLambdaLogEvent = event => { 137 | const msgParam = event.message; 138 | let msg = msgParam; 139 | const dateFormat = 'YYYY-MM-DD HH:mm:ss.SSS (Z)'; 140 | 141 | if (_.startsWith(msg, 'REPORT')) { 142 | msg += os.EOL; 143 | } 144 | 145 | if (_.startsWith(msg, 'START') || _.startsWith(msg, 'END') || _.startsWith(msg, 'REPORT')) { 146 | return chalk.gray(msg); 147 | } else if (_.trim(msg) === 'Process exited before completing request') { 148 | return chalk.red(msg); 149 | } 150 | 151 | const splitted = _.split(msg, '\t'); 152 | 153 | if (splitted.length < 3 || new Date(splitted[0]) === 'Invalid Date') { 154 | return msg; 155 | } 156 | const reqId = splitted[1]; 157 | const time = chalk.green(moment(splitted[0]).format(dateFormat)); 158 | const text = _.split(msg, `${reqId}\t`)[1]; 159 | 160 | return `${time}\t${chalk.yellow(reqId)}\t${text}`; 161 | }; 162 | 163 | return this.logsShowLogs(logStreamNames, formatLambdaLogEvent, this.logsGetLogStreams.bind(this)); 164 | }, 165 | 166 | logsShowLogs(logStreamNames, formatter, getLogStreams) { 167 | if (!logStreamNames || !logStreamNames.length) { 168 | if (this.options.tail) { 169 | return setTimeout((() => getLogStreams() 170 | .then(nextLogStreamNames => this.logsShowLogs(nextLogStreamNames, formatter, getLogStreams))), 171 | this.options.interval); 172 | } 173 | } 174 | 175 | const params = { 176 | logGroupName: this.options.logGroupName || this._apiLogsLogGroup, 177 | interleaved: true, 178 | logStreamNames, 179 | startTime: this.options.startTime, 180 | }; 181 | 182 | if (this.options.filter) params.filterPattern = this.options.filter; 183 | if (this.options.nextToken) params.nextToken = this.options.nextToken; 184 | if (this.options.startTime) { 185 | const since = _.includes(['m', 'h', 'd'], 186 | this.options.startTime[this.options.startTime.length - 1]); 187 | if (since) { 188 | params.startTime = moment().subtract( 189 | _.replace(this.options.startTime, /\D/g, ''), 190 | _.replace(this.options.startTime, /\d/g, '')).valueOf(); 191 | } else { 192 | params.startTime = moment.utc(this.options.startTime).valueOf(); 193 | } 194 | } 195 | 196 | return this.provider 197 | .request('CloudWatchLogs', 198 | 'filterLogEvents', 199 | params, 200 | this.options.stage, 201 | this.options.region) 202 | .then(results => { 203 | if (results.events) { 204 | _.forEach(results.events, e => { 205 | process.stdout.write(formatter(e)); 206 | }); 207 | } 208 | 209 | if (results.nextToken) { 210 | this.options.nextToken = results.nextToken; 211 | } else { 212 | delete this.options.nextToken; 213 | } 214 | 215 | if (this.options.tail) { 216 | if (results.events && results.events.length) { 217 | this.options.startTime = _.last(results.events).timestamp + 1; 218 | } 219 | 220 | return setTimeout((() => getLogStreams() 221 | .then(nextLogStreamNames => this.logsShowLogs(nextLogStreamNames, formatter, getLogStreams))), 222 | this.options.interval); 223 | } 224 | 225 | return BbPromise.resolve(); 226 | }); 227 | }, 228 | 229 | }; 230 | -------------------------------------------------------------------------------- /lib/removeAlias.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | const _ = require('lodash'); 5 | const utils = require('./utils'); 6 | 7 | const NO_UPDATE_MESSAGE = 'No updates are to be performed.'; 8 | 9 | module.exports = { 10 | 11 | aliasCreateStackChanges(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 12 | 13 | return BbPromise.try(() => { 14 | 15 | const usedFuncRefs = _.uniq( 16 | _.flatMap(aliasStackTemplates, template => { 17 | const funcRefs = _.map( 18 | _.assign({}, 19 | _.pickBy( 20 | _.get(template, 'Resources', {}), 21 | [ 'Type', 'AWS::Lambda::Alias' ])), 22 | (value, key) => { 23 | return _.replace(key, /Alias$/, ''); 24 | } 25 | ); 26 | return funcRefs; 27 | }) 28 | ); 29 | 30 | const usedResources = _.flatMap(aliasStackTemplates, template => { 31 | return JSON.parse(_.get(template, 'Outputs.AliasResources.Value', "[]")); 32 | }); 33 | 34 | const usedOutputs = _.flatMap(aliasStackTemplates, template => { 35 | return JSON.parse(_.get(template, 'Outputs.AliasOutputs.Value', "[]")); 36 | }); 37 | 38 | const obsoleteFuncRefs = _.reject(_.map( 39 | _.assign({}, 40 | _.pickBy( 41 | _.get(currentAliasStackTemplate, 'Resources', {}), 42 | [ 'Type', 'AWS::Lambda::Alias' ])), 43 | (value, key) => { 44 | return _.replace(key, /Alias$/, ''); 45 | }), ref => _.includes(usedFuncRefs, ref)); 46 | 47 | const obsoleteFuncResources = _.flatMap(obsoleteFuncRefs, 48 | name => ([ `${name}LambdaFunction`, `${name}LogGroup` ])); 49 | 50 | const obsoleteFuncOutputs = _.map(obsoleteFuncRefs, 51 | name => `${name}LambdaFunctionArn`); 52 | 53 | const obsoleteResources = _.reject( 54 | JSON.parse(_.get(currentAliasStackTemplate, 'Outputs.AliasResources.Value', "[]")), 55 | resource => _.includes(usedResources, resource)); 56 | 57 | const obsoleteOutputs = _.reject( 58 | JSON.parse(_.get(currentAliasStackTemplate, 'Outputs.AliasOutputs.Value', "[]")), 59 | output => _.includes(usedOutputs, output)); 60 | 61 | // Check for aliased authorizers thhat reference a removed function 62 | _.forEach(obsoleteFuncRefs, obsoleteFuncRef => { 63 | const authorizerName = `${obsoleteFuncRef}ApiGatewayAuthorizer${_.replace(this._alias, /-/g, 'Dash')}`; 64 | if (_.has(currentTemplate.Resources, authorizerName)) { 65 | // find obsolete references 66 | const authRefs = utils.findReferences(currentTemplate.Resources, authorizerName); 67 | _.forEach(authRefs, authRef => { 68 | if (_.endsWith(authRef, '.AuthorizerId')) { 69 | const parent = _.get(currentTemplate.Resources, _.replace(authRef, '.AuthorizerId', '')); 70 | delete parent.AuthorizerId; 71 | parent.AuthorizationType = "NONE"; 72 | } 73 | }); 74 | // find dependencies 75 | _.forOwn(currentTemplate.Resources, resource => { 76 | if (_.isArray(resource.DependsOn) && _.includes(resource.DependsOn, authorizerName)) { 77 | resource.DependsOn = _.without(resource.DependsOn, authorizerName); 78 | } else if (resource.DependsOn === authorizerName) { 79 | delete resource.DependsOn; 80 | } 81 | }); 82 | // Add authorizer to obsolete resources 83 | obsoleteResources.push(authorizerName); 84 | } 85 | }); 86 | 87 | // Remove all alias references that are not used in other stacks 88 | _.assign(currentTemplate, { 89 | Resources: _.assign({}, _.omit(currentTemplate.Resources, obsoleteFuncResources, obsoleteResources)), 90 | Outputs: _.assign({}, _.omit(currentTemplate.Outputs, obsoleteFuncOutputs, obsoleteOutputs)) 91 | }); 92 | 93 | if (this.options.verbose) { 94 | this._serverless.cli.log(`Remove unused resources:`); 95 | _.forEach(obsoleteResources, resource => this._serverless.cli.log(` * ${resource}`)); 96 | } 97 | 98 | this.options.verbose && this._serverless.cli.log(`Remove alias IAM policy`); 99 | // Remove the alias IAM policy if it is not referenced in the current stage stack 100 | // We cannot remove it otherwise, because the $LATEST function versions might still reference it. 101 | // Then it will be deleted on the next deployment or the stage removal, whatever happend first. 102 | const aliasPolicyName = `IamRoleLambdaExecution${this._alias}`; 103 | if (_.isEmpty(utils.findReferences(currentTemplate.Resources, aliasPolicyName))) { 104 | delete currentTemplate.Resources[`IamRoleLambdaExecution${this._alias}`]; 105 | } else { 106 | this._serverless.cli.log(`IAM policy removal delayed - will be removed on next deployment`); 107 | } 108 | 109 | // Adjust IAM policies 110 | const obsoleteRefs = _.concat(obsoleteFuncResources, obsoleteResources); 111 | 112 | // Set references to obsoleted resources in fct env to "REMOVED" in case 113 | // the alias that is removed was the last deployment of the stage. 114 | // This will change the function definition, but that does not matter 115 | // as is is neither aliased nor versioned 116 | _.forEach(_.filter(currentTemplate.Resources, [ 'Type', 'AWS::Lambda::Function' ]), func => { 117 | const refs = utils.findReferences(func, obsoleteRefs); 118 | _.forEach(refs, ref => _.set(func, ref, "REMOVED")); 119 | }); 120 | 121 | // Check if API is still referenced and remove it otherwise 122 | const usesApi = _.some(aliasStackTemplates, template => { 123 | return _.some(_.get(template, 'Resources', {}), [ 'Type', 'AWS::ApiGateway::Deployment' ]); 124 | }); 125 | if (!usesApi) { 126 | this.options.verbose && this._serverless.cli.log(`Remove API`); 127 | 128 | delete currentTemplate.Resources.ApiGatewayRestApi; 129 | delete currentTemplate.Outputs.ApiGatewayRestApi; 130 | delete currentTemplate.Outputs.ApiGatewayRestApiRootResource; 131 | delete currentTemplate.Outputs.ServiceEndpoint; 132 | } 133 | 134 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 135 | }); 136 | }, 137 | 138 | aliasApplyStackChanges(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 139 | 140 | const stackName = this._provider.naming.getStackName(); 141 | 142 | this.options.verbose && this._serverless.cli.log(`Apply changes for ${stackName}`); 143 | 144 | let stackTags = { STAGE: this._stage }; 145 | 146 | // Merge additional stack tags 147 | if (_.isObject(this.serverless.service.provider.stackTags)) { 148 | stackTags = _.extend(stackTags, this.serverless.service.provider.stackTags); 149 | } 150 | 151 | const params = { 152 | StackName: stackName, 153 | Capabilities: [ 154 | 'CAPABILITY_IAM', 155 | 'CAPABILITY_NAMED_IAM', 156 | ], 157 | Parameters: [], 158 | TemplateBody: JSON.stringify(currentTemplate), 159 | Tags: _.map(_.keys(stackTags), key => ({ Key: key, Value: stackTags[key] })), 160 | }; 161 | 162 | this.options.verbose && this._serverless.cli.log(`Checking stack policy`); 163 | 164 | // Policy must have at least one statement, otherwise no updates would be possible at all 165 | if (this.serverless.service.provider.stackPolicy && 166 | this.serverless.service.provider.stackPolicy.length) { 167 | params.StackPolicyBody = JSON.stringify({ 168 | Statement: this.serverless.service.provider.stackPolicy, 169 | }); 170 | } 171 | 172 | return this._provider.request('CloudFormation', 173 | 'updateStack', 174 | params, 175 | this.options.stage, 176 | this.options.region) 177 | .then(cfData => this.monitorStack('update', cfData)) 178 | .then(() => BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ])) 179 | .catch(err => { 180 | if (err.message === NO_UPDATE_MESSAGE) { 181 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 182 | } 183 | throw err; 184 | }); 185 | 186 | }, 187 | 188 | aliasRemoveAliasStack(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 189 | 190 | const stackName = `${this._provider.naming.getStackName()}-${this._alias}`; 191 | 192 | this.options.verbose && this._serverless.cli.log(`Removing CF stack ${stackName}`); 193 | 194 | return this._provider.request('CloudFormation', 195 | 'deleteStack', 196 | { StackName: stackName }, 197 | this._options.stage, 198 | this._options.region) 199 | .then(cfData => { 200 | // monitorStack wants a StackId member 201 | cfData.StackId = stackName; 202 | return this.monitorStack('removal', cfData); 203 | }) 204 | .then(() =>{ 205 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 206 | }) 207 | .catch(e => { 208 | if (_.includes(e.message, 'does not exist')) { 209 | const message = `Alias ${this._alias} is not deployed.`; 210 | throw new this._serverless.classes.Error(message); 211 | } 212 | 213 | throw e; 214 | }); 215 | 216 | }, 217 | 218 | removeAlias(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 219 | 220 | if (this._options.noDeploy) { 221 | this._serverless.cli.log('noDeploy option active - will do nothing'); 222 | return BbPromise.resolve(); 223 | } 224 | 225 | this._masterAlias = currentTemplate.Outputs.MasterAliasName.Value; 226 | if (this._stage && this._masterAlias === this._alias) { 227 | // Removal of the master alias is requested -> check if any other aliases are still deployed. 228 | const aliases = _.map(aliasStackTemplates, aliasTemplate => _.get(aliasTemplate, 'Outputs.ServerlessAliasName.Value')); 229 | if (!_.isEmpty(aliases)) { 230 | throw new this._serverless.classes.Error(`Remove the other deployed aliases before removing the service: ${_.without(aliases, this._masterAlias)}`); 231 | } 232 | if (_.isEmpty(currentAliasStackTemplate)) { 233 | throw new this._serverless.classes.Error(`Internal error: Stack for master alias ${this._masterAlias} is not deployed. Try to solve the problem by manual interaction with the AWS console.`); 234 | } 235 | 236 | // We're ready for removal 237 | this._serverless.cli.log(`Removing master alias and stage ${this._masterAlias} ...`); 238 | 239 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]).bind(this) 240 | .spread(this.aliasRemoveAliasStack) 241 | .then(() => this._serverless.pluginManager.spawn('remove')); 242 | } 243 | 244 | this._serverless.cli.log(`Removing alias ${this._masterAlias} ...`); 245 | 246 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]).bind(this) 247 | .spread(this.aliasCreateStackChanges) 248 | .spread(this.aliasRemoveAliasStack) 249 | .spread(this.aliasApplyStackChanges) 250 | .then(() => BbPromise.resolve()); 251 | 252 | } 253 | 254 | }; 255 | -------------------------------------------------------------------------------- /lib/stackInformation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper to retrieve and manage stack and alias information. 3 | */ 4 | 5 | const BbPromise = require('bluebird'); 6 | const _ = require('lodash'); 7 | 8 | module.exports = { 9 | 10 | /** 11 | * Load the currently deployed CloudFormation template. 12 | */ 13 | aliasStackLoadCurrentTemplate() { 14 | 15 | const stackName = this._provider.naming.getStackName(); 16 | 17 | const params = { 18 | StackName: stackName, 19 | TemplateStage: 'Processed' 20 | }; 21 | 22 | return this._provider.request('CloudFormation', 23 | 'getTemplate', 24 | params, 25 | this._options.stage, 26 | this._options.region) 27 | .then(cfData => { 28 | try { 29 | return BbPromise.resolve(JSON.parse(cfData.TemplateBody)); 30 | } catch (e) { 31 | return BbPromise.reject(new Error('Received malformed response from CloudFormation')); 32 | } 33 | }) 34 | .catch(() => { 35 | return BbPromise.resolve({ Resources: {}, Outputs: {} }); 36 | }); 37 | 38 | }, 39 | 40 | aliasStackGetAliasStackNames() { 41 | 42 | const params = { 43 | ExportName: `${this._provider.naming.getStackName()}-ServerlessAliasReference` 44 | }; 45 | 46 | return this._provider.request('CloudFormation', 47 | 'listImports', 48 | params, 49 | this._options.stage, 50 | this._options.region) 51 | .then(cfData => BbPromise.resolve(cfData.Imports)); 52 | 53 | }, 54 | 55 | aliasStackLoadTemplate(stackName, processed) { 56 | 57 | const params = { 58 | StackName: stackName, 59 | TemplateStage: processed ? 'Processed' : 'Original' 60 | }; 61 | 62 | return this._provider.request('CloudFormation', 63 | 'getTemplate', 64 | params, 65 | this._options.stage, 66 | this._options.region) 67 | .then(cfData => { 68 | return BbPromise.resolve(JSON.parse(cfData.TemplateBody)); 69 | }) 70 | .catch(err => { 71 | return BbPromise.reject(new Error(`Unable to retrieve template for ${stackName}: ${err.statusCode}`)); 72 | }); 73 | 74 | }, 75 | 76 | /** 77 | * Load all deployed alias stack templates excluding the current alias. 78 | */ 79 | aliasStackLoadAliasTemplates() { 80 | 81 | return this.aliasStackGetAliasStackNames() // eslint-disable-line lodash/prefer-lodash-method 82 | .mapSeries(stack => BbPromise.join(BbPromise.resolve(stack), this.aliasStackLoadTemplate(stack))) 83 | .map(stackInfo => ({ stack: stackInfo[0], template: stackInfo[1] })) 84 | .catch(err => { 85 | if (err.statusCode === 400) { 86 | // The export is not yet there. Can happen on the very first alias stack deployment. 87 | return BbPromise.resolve([]); 88 | } 89 | 90 | return BbPromise.reject(err); 91 | }); 92 | 93 | }, 94 | 95 | aliasStacksDescribeStage() { 96 | 97 | const stackName = this._provider.naming.getStackName(); 98 | 99 | return this._provider.request('CloudFormation', 100 | 'describeStackResources', 101 | { StackName: stackName }, 102 | this._options.stage, 103 | this._options.region); 104 | }, 105 | 106 | aliasStacksDescribeResource(resourceId) { 107 | 108 | const stackName = this._provider.naming.getStackName(); 109 | 110 | return this._provider.request('CloudFormation', 111 | 'describeStackResources', 112 | { 113 | StackName: stackName, 114 | LogicalResourceId: resourceId 115 | }, 116 | this._options.stage, 117 | this._options.region); 118 | }, 119 | 120 | aliasStacksDescribeAliases() { 121 | const params = { 122 | ExportName: `${this._provider.naming.getStackName()}-ServerlessAliasReference` 123 | }; 124 | 125 | return this._provider.request('CloudFormation', 126 | 'listImports', 127 | params, 128 | this._options.stage, 129 | this._options.region) 130 | .then(cfData => BbPromise.resolve(cfData.Imports)) 131 | .mapSeries(stack => { 132 | const describeParams = { 133 | StackName: stack 134 | }; 135 | 136 | return this._provider.request('CloudFormation', 137 | 'describeStackResources', 138 | describeParams, 139 | this._options.stage, 140 | this._options.region); 141 | }); 142 | }, 143 | 144 | aliasGetExports() { 145 | const fetchExports = (result, token) => { 146 | const params = {}; 147 | if (token) { 148 | params.NextToken = token; 149 | } 150 | return this._provider.request('CloudFormation', 'listExports', params) 151 | .then(cfData => { 152 | const newResult = _.reduce(cfData.Exports, (__, cfExport) => { 153 | __[cfExport.Name] = cfExport.Value; 154 | return __; 155 | }, result); 156 | 157 | if (cfData.NextToken) { 158 | return fetchExports(newResult, cfData.NextToken); 159 | } 160 | 161 | return newResult; 162 | }); 163 | }; 164 | 165 | return fetchExports({}); 166 | }, 167 | 168 | aliasStackLoadCurrentCFStackAndDependencies() { 169 | return BbPromise.join( 170 | BbPromise.bind(this).then(this.aliasStackLoadCurrentTemplate), 171 | BbPromise.bind(this).then(this.aliasStackLoadAliasTemplates) 172 | ) 173 | .spread((currentTemplate, aliasStackTemplates) => { 174 | const removed = _.filter(aliasStackTemplates, ['stack', `${this._provider.naming.getStackName()}-${this._alias}`]); 175 | const filteredAliasStackTemplates = _.reject(aliasStackTemplates, ['stack', `${this._provider.naming.getStackName()}-${this._alias}`]); 176 | 177 | const currentAliasStackTemplate = _.get(_.first(removed), 'template', {}); 178 | const deployedAliasStackTemplates = _.map(filteredAliasStackTemplates, template => template.template); 179 | 180 | this._serverless.service.provider.deployedCloudFormationTemplate = currentTemplate; 181 | this._serverless.service.provider.deployedCloudFormationAliasTemplate = currentAliasStackTemplate; 182 | this._serverless.service.provider.deployedAliasTemplates = filteredAliasStackTemplates; 183 | return BbPromise.resolve([ currentTemplate, deployedAliasStackTemplates, currentAliasStackTemplate ]); 184 | }); 185 | }, 186 | 187 | aliasDescribeAliasStack(aliasName) { 188 | const stackName = `${this._provider.naming.getStackName()}-${aliasName}`; 189 | return this._provider.request('CloudFormation', 190 | 'describeStackResources', 191 | { StackName: stackName }, 192 | this._options.stage, 193 | this._options.region); 194 | }, 195 | 196 | aliasGetAliasFunctionVersions(aliasName) { 197 | return this.aliasDescribeAliasStack(aliasName) 198 | .then(resources => { 199 | const versions = _.filter(resources.StackResources, [ 'ResourceType', 'AWS::Lambda::Version' ]); 200 | return _.map(versions, version => ({ 201 | functionName: /:function:(.*):/.exec(version.PhysicalResourceId)[1], 202 | functionVersion: _.last(_.split(version.PhysicalResourceId, ':')) 203 | })); 204 | }); 205 | }, 206 | 207 | aliasGetAliasLatestFunctionVersionByFunctionName(aliasName, functionName) { 208 | return this._provider.request('Lambda', 209 | 'getAlias', 210 | { FunctionName: functionName, Name: aliasName }, 211 | this._options.stage, 212 | this._options.region) 213 | .then(result => _.get(result, 'FunctionVersion', null)); 214 | }, 215 | 216 | }; 217 | -------------------------------------------------------------------------------- /lib/stackops/cwEvents.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Transform CW events. 4 | */ 5 | 6 | const BbPromise = require('bluebird'); 7 | const _ = require('lodash'); 8 | const utils = require('../utils'); 9 | 10 | module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 11 | 12 | const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate; 13 | const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate; 14 | 15 | const cwEvents = _.assign({}, _.pickBy(_.get(stageStack, 'Resources', {}), [ 'Type', 'AWS::Events::Rule' ])); 16 | const cwEventLambdaPermissions = 17 | _.assign({}, 18 | _.pickBy(_.pickBy(stageStack.Resources, [ 'Type', 'AWS::Lambda::Permission' ]), 19 | permission => utils.hasPermissionPrincipal(permission, 'events'))); 20 | 21 | _.forOwn(cwEvents, (cwEvent, name) => { 22 | // Reference alias as FunctionName 23 | const targetRefs = utils.findAllReferences(_.get(cwEvent, 'Properties.Targets')); 24 | cwEvent.DependsOn = cwEvent.DependsOn || []; 25 | _.forEach(targetRefs, ref => { 26 | const functionName = _.replace(ref.ref, /LambdaFunction$/, ''); 27 | _.set(cwEvent.Properties.Targets, ref.path, { Ref: `${functionName}Alias` }); 28 | cwEvent.DependsOn.push(`${functionName}Alias`); 29 | }); 30 | 31 | // Remove mapping from stage stack 32 | delete stageStack.Resources[name]; 33 | }); 34 | 35 | // Move event subscriptions to alias stack 36 | _.defaults(aliasStack.Resources, cwEvents); 37 | 38 | // Adjust permission to reference the function aliases 39 | _.forOwn(cwEventLambdaPermissions, (permission, name) => { 40 | const targetFunctionRef = utils.findAllReferences(_.get(permission, 'Properties.FunctionName')); 41 | const functionName = _.replace(targetFunctionRef[0].ref, /LambdaFunction$/, ''); 42 | 43 | // Adjust references and alias permissions 44 | permission.Properties.FunctionName = { Ref: `${functionName}Alias` }; 45 | 46 | // Add dependency on function alias 47 | permission.DependsOn = [ `${functionName}Alias` ]; 48 | 49 | delete stageStack.Resources[name]; 50 | }); 51 | 52 | // Add all alias stack owned resources 53 | _.defaults(aliasStack.Resources, cwEventLambdaPermissions); 54 | 55 | // Forward inputs to the promise chain 56 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 57 | }; 58 | -------------------------------------------------------------------------------- /lib/stackops/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Transform event source mappings, 4 | */ 5 | 6 | const BbPromise = require('bluebird'); 7 | const _ = require('lodash'); 8 | const utils = require('../utils'); 9 | 10 | module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 11 | 12 | const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate; 13 | const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate; 14 | const stackName = this._provider.naming.getStackName(); 15 | 16 | const subscriptions = _.assign({}, _.pickBy(_.get(stageStack, 'Resources', {}), [ 'Type', 'AWS::Lambda::EventSourceMapping' ])); 17 | 18 | this.options.verbose && this._serverless.cli.log('Processing event source subscriptions'); 19 | 20 | _.forOwn(subscriptions, (subscription, name) => { 21 | // Reference alias as FunctionName 22 | const functionNameRef = utils.findAllReferences(_.get(subscription, 'Properties.FunctionName')); 23 | const functionName = _.replace(_.get(functionNameRef, '[0].ref', ''), /LambdaFunction$/, ''); 24 | if (_.isEmpty(functionName)) { 25 | // FIXME: Can this happen at all? 26 | this._serverless.cli.log(`Strange thing: No function name defined for ${name}`); 27 | return; 28 | } 29 | 30 | subscription.Properties.FunctionName = { Ref: `${functionName}Alias` }; 31 | subscription.DependsOn = [ `${functionName}Alias` ]; 32 | 33 | // Make sure that the referenced resource is exported by the stageStack. 34 | const resourceRef = utils.findAllReferences(_.get(subscription, 'Properties.EventSourceArn')); 35 | // Build the export name 36 | let resourceRefName = _.get(resourceRef, '[0].ref'); 37 | if (_.has(subscription.Properties, 'EventSourceArn.Fn::GetAtt')) { 38 | const attribute = subscription.Properties.EventSourceArn['Fn::GetAtt'][1]; 39 | resourceRefName += attribute; 40 | } 41 | // Add the ref output to the stack if not already done. 42 | stageStack.Outputs[resourceRefName] = { 43 | Description: 'Alias resource reference', 44 | Value: subscription.Properties.EventSourceArn, 45 | Export: { 46 | Name: `${stackName}-${resourceRefName}` 47 | } 48 | }; 49 | // Add the output to the referenced alias outputs 50 | const aliasOutputs = JSON.parse(aliasStack.Outputs.AliasOutputs.Value); 51 | aliasOutputs.push(resourceRefName); 52 | aliasStack.Outputs.AliasOutputs.Value = JSON.stringify(aliasOutputs); 53 | // Replace the reference with the cross stack reference 54 | subscription.Properties.EventSourceArn = {}; 55 | 56 | // Event source ARNs can be volatile - e.g. DynamoDB and must not be referenced 57 | // with Fn::ImportValue which does not allow for changes. So we have to register 58 | // them for delayed lookup until the base stack has been updated. 59 | this.addDeferredOutput(`${stackName}-${resourceRefName}`, subscription.Properties, 'EventSourceArn'); 60 | 61 | // Remove mapping from stage stack 62 | delete stageStack.Resources[name]; 63 | }); 64 | 65 | // Move event subscriptions to alias stack 66 | _.defaults(aliasStack.Resources, subscriptions); 67 | 68 | // Forward inputs to the promise chain 69 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 70 | }; 71 | -------------------------------------------------------------------------------- /lib/stackops/functions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Transform frunctions and versions. 4 | */ 5 | 6 | const BbPromise = require('bluebird'); 7 | const _ = require('lodash'); 8 | const utils = require('../utils'); 9 | 10 | /** 11 | * Merge template definitions that are still in use into the new template 12 | * @param stackName {String} Main stack name 13 | * @param newTemplate {Object} New main stack template 14 | * @param currentTemplate {Object} Currently deployed main stack template 15 | * @param aliasStackTemplates {Array} Currently deployed and references aliases 16 | */ 17 | function mergeAliases(stackName, newTemplate, currentTemplate, aliasStackTemplates, currentAliasStackTemplate, removedResources) { 18 | 19 | const allAliasTemplates = _.concat(aliasStackTemplates, currentAliasStackTemplate); 20 | 21 | // Get all referenced function logical resource ids 22 | const aliasedFunctions = 23 | _.flatMap( 24 | allAliasTemplates, 25 | template => _.compact(_.map( 26 | template.Resources, 27 | (resource, name) => { 28 | if (resource.Type === 'AWS::Lambda::Alias') { 29 | return { 30 | name: _.replace(name, /Alias$/, 'LambdaFunction'), 31 | version: _.replace(_.get(resource, 'Properties.FunctionVersion.Fn::ImportValue'), `${stackName}-`, '') 32 | }; 33 | } 34 | return null; 35 | } 36 | )) 37 | ); 38 | 39 | // Get currently deployed function definitions and versions and retain them in the stack update 40 | const usedFunctionElements = { 41 | Resources: _.map(aliasedFunctions, aliasedFunction => _.assign( 42 | {}, 43 | _.pick(currentTemplate.Resources, [ aliasedFunction.name, aliasedFunction.version ]) 44 | )), 45 | Outputs: _.map(aliasedFunctions, aliasedFunction => _.assign( 46 | {}, 47 | _.pick(currentTemplate.Outputs, [ `${aliasedFunction.name}Arn`, aliasedFunction.version ]) 48 | )) 49 | }; 50 | 51 | _.forEach(usedFunctionElements.Resources, resources => _.defaults(newTemplate.Resources, resources)); 52 | _.forEach(usedFunctionElements.Outputs, outputs => _.defaults(newTemplate.Outputs, outputs)); 53 | 54 | // Set references to obsoleted resources in fct env to "REMOVED" in case 55 | // the alias that is removed was the last deployment of the stage. 56 | // This will change the function definition, but that does not matter 57 | // as is is neither aliased nor versioned 58 | _.forEach(_.filter(newTemplate.Resources, [ 'Type', 'AWS::Lambda::Function' ]), func => { 59 | const refs = utils.findReferences(func, removedResources); 60 | _.forEach(refs, ref => _.set(func, ref, "REMOVED")); 61 | }); 62 | 63 | } 64 | 65 | module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 66 | 67 | this.options.verbose && this._serverless.cli.log('Processing functions'); 68 | 69 | const stackName = this._provider.naming.getStackName(); 70 | const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate; 71 | const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate; 72 | 73 | /** 74 | * Add the stage stack reference to the alias stack. 75 | * This makes sure that the stacks are linked together. 76 | */ 77 | aliasStack.Outputs.ServerlessAliasReference = { 78 | Description: 'Alias stack reference.', 79 | Value: { 80 | 'Fn::ImportValue': `${this._provider.naming.getStackName()}-ServerlessAliasReference` 81 | } 82 | }; 83 | 84 | // Set SERVERLESS_ALIAS environment variable 85 | _.forOwn(stageStack.Resources, resource => { 86 | if (resource.Type === 'AWS::Lambda::Function') { 87 | _.set(resource, 'Properties.Environment.Variables.SERVERLESS_ALIAS', this._alias); 88 | } 89 | }); 90 | 91 | const versions = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::Lambda::Version' ])); 92 | if (!_.isEmpty(versions)) { 93 | 94 | // The alias stack will be the owner of the versioned functions 95 | _.forOwn(versions, (version, versionName) => { 96 | 97 | const functionName = _.replace(_.get(version, 'Properties.FunctionName.Ref'), /LambdaFunction$/, ''); 98 | 99 | // Remove the function version export 100 | delete stageStack.Outputs[`${functionName}LambdaFunctionQualifiedArn`]; 101 | 102 | // Add function Arn export to stage stack 103 | stageStack.Outputs[`${functionName}LambdaFunctionArn`] = { 104 | Description: 'Function Arn', 105 | Value: { 'Fn::GetAtt': [ `${functionName}LambdaFunction`, 'Arn' ] }, // Ref: `${name}LambdaFunction` } 106 | Export: { 107 | Name: `${stackName}-${functionName}-LambdaFunctionArn` 108 | } 109 | }; 110 | 111 | // Reference correct function name in version 112 | version.Properties.FunctionName = { 'Fn::ImportValue': `${stackName}-${functionName}-LambdaFunctionArn` }; 113 | 114 | // With alias support we do not want to retain the versions unless explicitly asked to do so 115 | version.DeletionPolicy = this._retain ? 'Retain' : 'Delete'; 116 | 117 | // Add alias to alias stack. Reference the version export in the stage stack 118 | // to prevent version deletion. 119 | const alias = { 120 | Type: 'AWS::Lambda::Alias', 121 | Properties: { 122 | Description: _.get(stageStack.Resources, `${functionName}LambdaFunction.Properties.Description`), 123 | FunctionName: { 124 | 'Fn::ImportValue': `${stackName}-${functionName}-LambdaFunctionArn` 125 | }, 126 | FunctionVersion: { 'Fn::GetAtt': [ versionName, 'Version' ] }, 127 | Name: this._alias 128 | }, 129 | DependsOn: [ 130 | versionName 131 | ] 132 | }; 133 | 134 | aliasStack.Resources[`${functionName}Alias`] = alias; 135 | 136 | delete stageStack.Resources[versionName]; 137 | }); 138 | 139 | _.assign(aliasStack.Resources, versions); 140 | } 141 | 142 | // Merge function aliases and versions 143 | mergeAliases(stackName, stageStack, currentTemplate, aliasStackTemplates, currentAliasStackTemplate, this.removedResourceKeys); 144 | 145 | // FIXME: Resource handling 146 | // mergeResources() 147 | 148 | // Promote the parsed templates to the promise chain. 149 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 150 | }; 151 | -------------------------------------------------------------------------------- /lib/stackops/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Initialize and prepare stack restructuring 5 | */ 6 | 7 | const _ = require('lodash'); 8 | const BbPromise = require('bluebird'); 9 | 10 | const defaultAliasFlags = { 11 | hasRole: false 12 | }; 13 | 14 | module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 15 | const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate; 16 | 17 | // Prepare flags 18 | aliasStack.Outputs.AliasFlags = { 19 | Description: 'Alias flags.', 20 | Value: _.assign({}, defaultAliasFlags) 21 | }; 22 | 23 | _.forEach(aliasStackTemplates, aliasTemplate => { 24 | const flags = _.get(aliasTemplate, 'Outputs.AliasFlags', '{}'); 25 | try { 26 | _.set(aliasTemplate, 'Outputs.AliasFlags.Value', _.defaults(JSON.parse(flags), defaultAliasFlags)); 27 | } catch (e) { 28 | // Not handled 29 | } 30 | }); 31 | 32 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 33 | }; 34 | -------------------------------------------------------------------------------- /lib/stackops/lambdaRole.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Transform lambda role. 4 | * Merge alias and current stack policies, so that all alias policy statements 5 | * are present and active 6 | */ 7 | 8 | const BbPromise = require('bluebird'); 9 | const _ = require('lodash'); 10 | const utils = require('../utils'); 11 | 12 | module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 13 | 14 | const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate; 15 | const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate; 16 | 17 | // There can be a service role defined. In this case there is no embedded IAM role. 18 | if (_.has(this._serverless.service.provider, 'role')) { 19 | // Use the role if any of the aliases reference it 20 | aliasStack.Outputs.AliasFlags.Value.hasRole = true; 21 | 22 | // Import all defined roles from the current template (without overwriting) 23 | const currentRoles = _.assign({}, _.pickBy(currentTemplate.Resources, (resource, name) => resource.Type === 'AWS::IAM::Role' && /^IamRoleLambdaExecution/.test(name))); 24 | _.defaults(stageStack.Resources, currentRoles); 25 | 26 | // Remove old role for this alias 27 | delete stageStack.Resources[`IamRoleLambdaExecution${this._alias}`]; 28 | 29 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 30 | } 31 | 32 | // Role name allows [\w+=,.@-]+ 33 | const normalizedAlias = utils.normalizeAliasForLogicalId(this._alias); 34 | const roleLogicalId = `IamRoleLambdaExecution${normalizedAlias}`; 35 | const role = stageStack.Resources.IamRoleLambdaExecution; 36 | 37 | // Set role name 38 | if (role.Properties.RoleName['Fn::Join']) { 39 | _.last(role.Properties.RoleName['Fn::Join']).push(this._alias); 40 | } 41 | 42 | stageStack.Resources[roleLogicalId] = stageStack.Resources.IamRoleLambdaExecution; 43 | delete stageStack.Resources.IamRoleLambdaExecution; 44 | 45 | // Replace references 46 | const functions = _.filter(stageStack.Resources, ['Type', 'AWS::Lambda::Function']); 47 | 48 | const functionsWithIamRoleReference = _.filter(functions, (func) => _.isEqual( 49 | func.Properties.Role, 50 | {'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn' ]} 51 | )); 52 | 53 | _.forEach(functionsWithIamRoleReference, func => { 54 | func.Properties.Role = { 55 | 'Fn::GetAtt': [ 56 | roleLogicalId, 57 | 'Arn' 58 | ] 59 | }; 60 | const dependencyIndex = _.indexOf(func.DependsOn, 'IamRoleLambdaExecution'); 61 | func.DependsOn[dependencyIndex] = roleLogicalId; 62 | }); 63 | 64 | if (_.has(currentTemplate, 'Resources.IamRoleLambdaExecution')) { 65 | if (!_.isEmpty(utils.findReferences(currentTemplate.Resources, 'IamRoleLambdaExecution'))) { 66 | stageStack.Resources.IamRoleLambdaExecution = currentTemplate.Resources.IamRoleLambdaExecution; 67 | } 68 | delete currentTemplate.Resources.IamRoleLambdaExecution; 69 | } 70 | 71 | // Retain the roles of all currently deployed aliases 72 | _.forEach(aliasStackTemplates, aliasTemplate => { 73 | const alias = _.get(aliasTemplate, 'Outputs.ServerlessAliasName.Value'); 74 | const aliasNormalizedAlias = utils.normalizeAliasForLogicalId(alias); 75 | const aliasRoleLogicalId = `IamRoleLambdaExecution${aliasNormalizedAlias}`; 76 | const aliasRole = _.get(currentTemplate, `Resources.${aliasRoleLogicalId}`); 77 | if (alias && aliasRole) { 78 | stageStack.Resources[aliasRoleLogicalId] = aliasRole; 79 | } 80 | }); 81 | 82 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 83 | }; 84 | -------------------------------------------------------------------------------- /lib/stackops/snsEvents.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Handle SNS Lambda subscriptions. 5 | */ 6 | 7 | const _ = require('lodash'); 8 | const BbPromise = require('bluebird'); 9 | const utils = require('../utils'); 10 | 11 | module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 12 | const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate; 13 | const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate; 14 | 15 | this.options.verbose && this._serverless.cli.log('Processing SNS Lambda subscriptions'); 16 | 17 | const aliasResources = []; 18 | 19 | const aliases = _.assign({}, _.pickBy(aliasStack.Resources, [ 'Type', 'AWS::Lambda::Alias' ])); 20 | const versions = _.assign({}, _.pickBy(aliasStack.Resources, [ 'Type', 'AWS::Lambda::Version' ])); 21 | 22 | // Add alias name to topics to disambiguate behavior 23 | const snsTopics = 24 | _.assign({}, 25 | _.pickBy(stageStack.Resources, [ 'Type', 'AWS::SNS::Topic' ])); 26 | 27 | _.forOwn(snsTopics, (topic, name) => { 28 | topic.DependsOn = topic.DependsOn || []; 29 | // Remap lambda subscriptions 30 | const lambdaSubscriptions = _.pickBy(topic.Properties.Subscription, ['Protocol', 'lambda']); 31 | _.forOwn(lambdaSubscriptions, subscription => { 32 | const functionNameRef = utils.findAllReferences(_.get(subscription, 'Endpoint')); 33 | const functionName = _.replace(_.get(functionNameRef, '[0].ref', ''), /LambdaFunction$/, ''); 34 | const versionName = utils.getFunctionVersionName(versions, functionName); 35 | const aliasName = utils.getAliasVersionName(aliases, functionName); 36 | 37 | subscription.Endpoint = { Ref: aliasName }; 38 | 39 | // Add dependency on function version 40 | topic.DependsOn.push(versionName); 41 | topic.DependsOn.push(aliasName); 42 | }); 43 | 44 | topic.Properties.TopicName = `${topic.Properties.TopicName}-${this._alias}`; 45 | 46 | delete stageStack.Resources[name]; 47 | }); 48 | 49 | const snsSubscriptions = 50 | _.assign({}, 51 | _.pickBy(stageStack.Resources, [ 'Type', 'AWS::SNS::Subscription' ])); 52 | 53 | _.forOwn(snsSubscriptions, (subscription, name) => { 54 | 55 | const functionNameRef = utils.findAllReferences(_.get(subscription.Properties, 'Endpoint')); 56 | const functionName = _.replace(_.get(functionNameRef, '[0].ref', ''), /LambdaFunction$/, ''); 57 | const versionName = utils.getFunctionVersionName(versions, functionName); 58 | const aliasName = utils.getAliasVersionName(aliases, functionName); 59 | 60 | subscription.Properties.Endpoint = { Ref: aliasName }; 61 | subscription.DependsOn = [ versionName, aliasName ]; 62 | 63 | delete stageStack.Resources[name]; 64 | }); 65 | 66 | // Fetch lambda permissions. These have to be updated later to allow the aliased functions. 67 | const snsLambdaPermissions = 68 | _.assign({}, 69 | _.pickBy(_.pickBy(stageStack.Resources, [ 'Type', 'AWS::Lambda::Permission' ]), 70 | permission => utils.hasPermissionPrincipal(permission, 'sns'))); 71 | 72 | // Adjust permission to reference the function aliases 73 | _.forOwn(snsLambdaPermissions, (permission, name) => { 74 | const functionName = _.replace(name, /LambdaPermission.*$/, ''); 75 | const versionName = utils.getFunctionVersionName(versions, functionName); 76 | const aliasName = utils.getAliasVersionName(aliases, functionName); 77 | 78 | // Adjust references and alias permissions 79 | permission.Properties.FunctionName = { Ref: aliasName }; 80 | const sourceArn = _.get(permission.Properties, 'SourceArn.Fn::Join[1]', []); 81 | sourceArn.push(`-${this._alias}`); 82 | 83 | // Add dependency on function version 84 | permission.DependsOn = [ versionName, aliasName ]; 85 | 86 | delete stageStack.Resources[name]; 87 | }); 88 | 89 | // Add all alias stack owned resources 90 | aliasResources.push(snsTopics); 91 | aliasResources.push(snsSubscriptions); 92 | aliasResources.push(snsLambdaPermissions); 93 | 94 | _.forEach(aliasResources, resource => _.assign(aliasStack.Resources, resource)); 95 | 96 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 97 | }; 98 | -------------------------------------------------------------------------------- /lib/stackops/userResources.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Handle user resources. 5 | * Keep all resources that are used somewhere and remove the ones that are not 6 | * referenced anymore. 7 | */ 8 | 9 | const _ = require('lodash'); 10 | const BbPromise = require('bluebird'); 11 | 12 | module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { 13 | 14 | const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate; 15 | const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate; 16 | const userResources = _.get(this._serverless.service, 'provider.aliasUserResources', { Resources: {}, Outputs: {} }); 17 | 18 | this.options.verbose && this._serverless.cli.log('Processing custom resources'); 19 | 20 | // Retrieve all resources referenced from other aliases 21 | const aliasDependencies = _.reduce(aliasStackTemplates, (result, template) => { 22 | try { 23 | const resourceRefs = JSON.parse(_.get(template, 'Outputs.AliasResources.Value', "[]")); 24 | const outputRefs = JSON.parse(_.get(template, 'Outputs.AliasOutputs.Value', "[]")); 25 | const resources = _.assign({}, _.pick(_.get(currentTemplate, 'Resources'), resourceRefs, {})); 26 | const outputs = _.assign({}, _.pick(_.get(currentTemplate, 'Outputs'), outputRefs, {})); 27 | 28 | _.assign(result.Resources, resources); 29 | _.assign(result.Outputs, outputs); 30 | return result; 31 | } catch (e) { 32 | return result; 33 | } 34 | }, { Resources: {}, Outputs: {} }); 35 | 36 | // Logical resource ids are unique per stage 37 | // Alias stacks reference the used resources through an Output reference 38 | // On deploy, the plugin checks if a resource is already deployed from a stack 39 | // and does a validation of the resource properties 40 | // All used resources are copied from the current template 41 | 42 | // Extract the user resources that are not overrides of existing Serverless resources 43 | const currentResources = _.get(userResources, 'Resources', {}); 44 | const currentOutputs = _.get(userResources, 'Outputs', {}); 45 | 46 | const aliasResourceRefs = _.keys(aliasDependencies.Resources); 47 | //const aliasOutputRefs = _.keys(aliasDependencies.Outputs); 48 | const currentResourceRefs = _.keys(currentResources); 49 | //const currentOutputRefs = _.keys(currentOutputs); 50 | const oldResourceRefs = JSON.parse(_.get(currentAliasStackTemplate, 'Outputs.AliasResources.Value', "[]")); 51 | //const oldOutputRefs = JSON.parse(_.get(currentAliasStackTemplate, 'Outputs.AliasOutputs.Value', "[]")); 52 | 53 | // Removed resources are resources that are no longer referenced 54 | 55 | const removedResourceRefs = _.difference(_.difference(oldResourceRefs, currentResourceRefs), aliasResourceRefs); 56 | this.removedResourceKeys = removedResourceRefs; 57 | this._options.verbose && this._serverless.cli.log(`Removing resources: ${this.removedResourceKeys}`); 58 | 59 | // Add the alias resources as output to the alias stack 60 | aliasStack.Outputs.AliasResources = { 61 | Description: 'Custom resource references', 62 | Value: JSON.stringify(_.keys(currentResources)) 63 | }; 64 | 65 | // Add the outputs as output to the alias stack 66 | aliasStack.Outputs.AliasOutputs = { 67 | Description: 'Custom output references', 68 | Value: JSON.stringify(_.keys(currentOutputs)) 69 | }; 70 | 71 | // FIXME: Deployments to the master (stage) alias should be allowed to reconfigure 72 | // resources and outputs. Otherwise a "merge" of feature branches into a 73 | // release branch would not be possible as resources would be rendered 74 | // immutable otherwise. 75 | 76 | // Check if the resource is already used anywhere else with a different definition 77 | _.forOwn(currentResources, (resource, name) => { 78 | if (_.has(aliasDependencies.Resources, name) && !_.isMatch(aliasDependencies.Resources[name], resource)) { 79 | 80 | // If we deploy the master alias, allow reconfiguration of resources 81 | if (this._alias === this._stage && resource.Type === aliasDependencies.Resources[name].Type) { 82 | this._serverless.cli.log(`Reconfigure resource ${name}. Remember to update it in other aliases too.`); 83 | } else { 84 | return BbPromise.reject(new Error(`Resource ${name} is already deployed in another alias with a different configuration. Either you change your resource to match the other definition, or you change the logical resource id to deploy your resource separately.`)); 85 | } 86 | } 87 | }); 88 | 89 | // Check if the output is already used anywhere else with a different definition 90 | _.forOwn(currentOutputs, (output, name) => { 91 | if (_.has(aliasDependencies.Outputs, name) && !_.isMatch(aliasDependencies.Outputs[name], output)) { 92 | if (this._alias === this._stage) { 93 | this._serverless.cli.log(`Reconfigure output ${name}. Remember to update it in other aliases too.`); 94 | } else { 95 | return BbPromise.reject(new Error(`Output ${name} is already deployed in another alias with a different configuration. Either you change your output to match the other definition, or you change the logical resource id to deploy your output separately.`)); 96 | } 97 | } 98 | }); 99 | 100 | // Merge used alias resources and outputs into the stage 101 | _.defaults(stageStack.Resources, aliasDependencies.Resources); 102 | _.defaults(stageStack.Outputs, aliasDependencies.Outputs); 103 | //console.log(JSON.stringify(aliasDependencies, null, 2)); 104 | //throw new Error('iwzgeiug'); 105 | 106 | return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); 107 | 108 | }; 109 | -------------------------------------------------------------------------------- /lib/updateAliasStack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const path = require('path'); 5 | const BbPromise = require('bluebird'); 6 | 7 | const NO_UPDATE_MESSAGE = 'No updates are to be performed.'; 8 | 9 | module.exports = { 10 | 11 | createAliasFallback() { 12 | this._createLater = false; 13 | this._serverless.cli.log('Creating alias stack...'); 14 | 15 | const stackName = `${this._provider.naming.getStackName()}-${this._alias}`; 16 | let stackTags = { STAGE: this._options.stage, ALIAS: this._alias }; 17 | const templateUrl = `https://s3.amazonaws.com/${this.bucketName}/${this._serverless.service.package.artifactDirectoryName}/compiled-cloudformation-template-alias.json`; 18 | // Merge additional stack tags 19 | if (_.isObject(this._serverless.service.provider.stackTags)) { 20 | stackTags = _.extend(stackTags, this._serverless.service.provider.stackTags); 21 | } 22 | 23 | const params = { 24 | StackName: stackName, 25 | OnFailure: 'DELETE', 26 | Capabilities: [ 27 | 'CAPABILITY_IAM', 28 | 'CAPABILITY_NAMED_IAM', 29 | ], 30 | Parameters: [], 31 | TemplateURL: templateUrl, 32 | Tags: _.map(_.keys(stackTags), key => ({ Key: key, Value: stackTags[key] })), 33 | }; 34 | 35 | if (this.serverless.service.provider.cfnRole) { 36 | params.RoleARN = this.serverless.service.provider.cfnRole; 37 | } 38 | 39 | return this._provider.request('CloudFormation', 40 | 'createStack', 41 | params, 42 | this._options.stage, 43 | this._options.region) 44 | .then((cfData) => this.monitorStack('create', cfData)); 45 | }, 46 | 47 | updateAlias() { 48 | const templateUrl = `https://s3.amazonaws.com/${this.bucketName}/${this._serverless.service.package.artifactDirectoryName}/compiled-cloudformation-template-alias.json`; 49 | 50 | this.serverless.cli.log('Updating alias stack...'); 51 | const stackName = `${this._provider.naming.getStackName()}-${this._alias}`; 52 | let stackTags = { STAGE: this._options.stage, ALIAS: this._alias }; 53 | 54 | // Merge additional stack tags 55 | if (_.isObject(this._serverless.service.provider.stackTags)) { 56 | stackTags = _.extend(stackTags, this.serverless.service.provider.stackTags); 57 | } 58 | 59 | const params = { 60 | StackName: stackName, 61 | Capabilities: [ 62 | 'CAPABILITY_IAM', 63 | 'CAPABILITY_NAMED_IAM', 64 | ], 65 | Parameters: [], 66 | TemplateURL: templateUrl, 67 | Tags: _.map(_.keys(stackTags), key => ({ Key: key, Value: stackTags[key] })), 68 | }; 69 | 70 | if (this.serverless.service.provider.cfnRole) { 71 | params.RoleARN = this.serverless.service.provider.cfnRole; 72 | } 73 | 74 | // Policy must have at least one statement, otherwise no updates would be possible at all 75 | if (this._serverless.service.provider.stackPolicy && 76 | this._serverless.service.provider.stackPolicy.length) { 77 | params.StackPolicyBody = JSON.stringify({ 78 | Statement: this._serverless.service.provider.stackPolicy, 79 | }); 80 | } 81 | 82 | return this._provider.request('CloudFormation', 83 | 'updateStack', 84 | params, 85 | this._options.stage, 86 | this._options.region) 87 | .then((cfData) => this.monitorStack('update', cfData)) 88 | .catch((e) => { 89 | if (e.message === NO_UPDATE_MESSAGE) { 90 | return; 91 | } 92 | throw e; 93 | }); 94 | }, 95 | 96 | updateAliasStack() { 97 | 98 | // just write the template to disk if a deployment should not be performed 99 | return BbPromise.bind(this) 100 | .then(this.writeAliasUpdateTemplateToDisk) 101 | .then(() => { 102 | if (this.options.noDeploy) { 103 | return BbPromise.resolve(); 104 | } else if (this._createLater) { 105 | return BbPromise.bind(this) 106 | .then(this.createAliasFallback); 107 | } 108 | return BbPromise.bind(this) 109 | .then(this.updateAlias); 110 | }); 111 | }, 112 | 113 | // helper methods 114 | writeAliasUpdateTemplateToDisk() { 115 | const updateOrCreate = this._createLater ? 'create' : 'update'; 116 | const cfTemplateFilePath = path.join(this._serverless.config.servicePath, 117 | '.serverless', `cloudformation-template-${updateOrCreate}-alias-stack.json`); 118 | 119 | this._serverless.utils.writeFileSync(cfTemplateFilePath, 120 | this._serverless.service.provider.compiledCloudFormationAliasTemplate); 121 | 122 | return BbPromise.resolve(); 123 | } 124 | 125 | }; 126 | -------------------------------------------------------------------------------- /lib/updateFunctionAlias.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | const _ = require('lodash'); 5 | 6 | module.exports = { 7 | 8 | updateFunctionAlias() { 9 | this._serverless.cli.log('Updating function alias...'); 10 | 11 | const func = this.serverless.service.getFunction(this.options.function); 12 | 13 | // Publish the yet deployed $LATEST uploaded by Serverless and label it. 14 | 15 | return BbPromise.try(() => { 16 | // Get the hash of the deployed function package 17 | const params = { 18 | FunctionName: func.name, 19 | Qualifier: '$LATEST' 20 | }; 21 | 22 | return this.provider.request( 23 | 'Lambda', 24 | 'getFunction', 25 | params, 26 | this.options.stage, this.options.region 27 | ); 28 | }) 29 | .then(result => { 30 | // Publish $LATEST 31 | const sha256 = result.Configuration.CodeSha256; 32 | const params = { 33 | FunctionName: func.name, 34 | CodeSha256: sha256, 35 | Description: 'Deployed manually' 36 | }; 37 | return this.provider.request( 38 | 'Lambda', 39 | 'publishVersion', 40 | params, 41 | this.options.stage, this.options.region 42 | ); 43 | }) 44 | .then(result => { 45 | // Label it 46 | const version = result.Version; 47 | const params = { 48 | FunctionName: func.name, 49 | Name: this._alias, 50 | FunctionVersion: version, 51 | Description: 'Deployed manually' 52 | }; 53 | return this.provider.request( 54 | 'Lambda', 55 | 'updateAlias', 56 | params, 57 | this.options.stage, this.options.region 58 | ); 59 | }) 60 | .then(result => { 61 | this.serverless.cli.log(_.join( 62 | [ 63 | 'Successfully updated alias: ', 64 | this.options.function, 65 | '@', 66 | this._alias, 67 | ' -> ', 68 | result.FunctionVersion 69 | ], 70 | '' 71 | )); 72 | return BbPromise.resolve(); 73 | }); 74 | } 75 | 76 | }; 77 | -------------------------------------------------------------------------------- /lib/uploadAliasArtifacts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | const _ = require('lodash'); 5 | 6 | module.exports = { 7 | uploadAliasCloudFormationFile() { 8 | this.serverless.cli.log('Uploading CloudFormation alias file to S3...'); 9 | 10 | const body = JSON.stringify(this.serverless.service.provider.compiledCloudFormationAliasTemplate); 11 | 12 | const fileName = 'compiled-cloudformation-template-alias.json'; 13 | 14 | let params = { 15 | Bucket: this.bucketName, 16 | Key: `${this.serverless.service.package.artifactDirectoryName}/${fileName}`, 17 | Body: body, 18 | ContentType: 'application/json', 19 | }; 20 | 21 | const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject; 22 | if (deploymentBucketObject) { 23 | params = setServersideEncryptionOptions(params, deploymentBucketObject); 24 | } 25 | 26 | return this.provider.request('S3', 27 | 'putObject', 28 | params, 29 | this._options.stage, 30 | this._options.region); 31 | }, 32 | 33 | uploadAliasArtifacts() { 34 | if (this.options.noDeploy) { 35 | return BbPromise.resolve(); 36 | } 37 | 38 | return BbPromise.bind(this) 39 | .then(this.resolveDeferredOutputs) 40 | .then(this.uploadAliasCloudFormationFile); 41 | }, 42 | 43 | }; 44 | 45 | function setServersideEncryptionOptions(putParams, deploymentBucketOptions) { 46 | const encryptionFields = { 47 | 'serverSideEncryption': 'ServerSideEncryption', 48 | 'sseCustomerAlgorithm': 'SSECustomerAlgorithm', 49 | 'sseCustomerKey': 'SSECustomerKey', 50 | 'sseCustomerKeyMD5': 'SSECustomerKeyMD5', 51 | 'sseKMSKeyId': 'SSEKMSKeyId', 52 | }; 53 | 54 | const params = putParams; 55 | 56 | _.forOwn(encryptionFields, (value, field) => { 57 | if (deploymentBucketOptions[field]) { 58 | params[value] = deploymentBucketOptions[field]; 59 | } 60 | }); 61 | 62 | return params; 63 | } 64 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Utilities and helpers 4 | */ 5 | 6 | const _ = require('lodash'); 7 | 8 | class Utils { 9 | 10 | /** 11 | * Find all given references in an object and return the paths to the 12 | * enclosing object. 13 | * @param root {Object} Start object 14 | * @param references {Array} References to search for 15 | * @returns {Array} Paths where the references are found 16 | */ 17 | static findReferences(root, references) { 18 | const resourcePaths = []; 19 | const stack = [ { parent: null, value: root, path:'' } ]; 20 | 21 | while (!_.isEmpty(stack)) { 22 | const property = stack.pop(); 23 | 24 | _.forOwn(property.value, (value, key) => { 25 | if (key === 'Ref' && _.includes(references, value) || 26 | key === 'Fn::GetAtt' && _.includes(references, value[0])) { 27 | resourcePaths.push(property.path); 28 | } else if (_.isObject(value)) { 29 | key = _.isArray(property.value) ? `[${key}]` : (_.isEmpty(property.path) ? `${key}` : `.${key}`); 30 | stack.push({ parent: property, value, path: `${property.path}${key}` }); 31 | } 32 | }); 33 | } 34 | 35 | return resourcePaths; 36 | } 37 | 38 | /** 39 | * Find AWS CF references in an object and return the referenced resources, including 40 | * the referenced resource and the enclosing object of the reference. 41 | * The referencing object can directly be retrieved with _.get(root, reference.path) 42 | * @param root {Object} Start object 43 | * @returns {Array} Found references as { ref: "", path: "" } 44 | */ 45 | static findAllReferences(root) { 46 | const resourceRefs = []; 47 | const stack = [ { parent: null, value: root, path: '' } ]; 48 | 49 | while (!_.isEmpty(stack)) { 50 | const property = stack.pop(); 51 | 52 | _.forOwn(property.value, (value, key) => { 53 | if (key === 'Ref') { 54 | resourceRefs.push({ ref: value, path: property.path }); 55 | } else if (key === 'Fn::GetAtt') { 56 | resourceRefs.push({ ref: value[0], path: property.path }); 57 | } else if (_.isObject(value)) { 58 | key = _.isArray(property.value) ? `[${key}]` : (_.isEmpty(property.path) ? `${key}` : `.${key}`); 59 | stack.push({ parent: property, value, path: `${property.path}${key}` }); 60 | } 61 | }); 62 | } 63 | 64 | return resourceRefs; 65 | } 66 | 67 | static normalizeAliasForLogicalId(alias) { 68 | // Only normalize if not alphanumeric 69 | if (_.isNil(alias) || /^[A-Za-z0-9]+$/.test(alias)) { 70 | return alias; 71 | } 72 | 73 | // Error on not supported characters 74 | if (!/^[A-Za-z0-9\-+_]+$/.test(alias)) { 75 | throw new Error("Unsupported character in alias. Must match [A-Za-z0-9\\-+_]+"); 76 | } 77 | 78 | const replacements = [ 79 | [ /-/g, 'Dash' ], 80 | [ /\+/g, 'Plus' ], 81 | [ /_/g, 'Uscore' ], 82 | ]; 83 | 84 | // Execute all replacements 85 | return _.reduce(replacements, (__, replacement) => { 86 | return _.replace(__, replacement[0], replacement[1]); 87 | }, alias); 88 | } 89 | 90 | /** 91 | * Checks if a CF resource permission targets the given service as Principal. 92 | * @param {Object} permission 93 | * @param {string} service 94 | */ 95 | static hasPermissionPrincipal(permission, service) { 96 | const principal = _.get(permission, 'Properties.Principal'); 97 | if (_.isString(principal)) { 98 | return _.startsWith(principal, service); 99 | } else if (_.isPlainObject(principal)) { 100 | const join = principal['Fn::Join']; 101 | if (join) { 102 | return _.some(join[1], joinPart => _.isString(joinPart) && _.startsWith(joinPart, service)); 103 | } 104 | } 105 | return false; 106 | } 107 | 108 | /** 109 | * @param {object} versions 110 | * @param {string} functionName 111 | * @returns {string} 112 | */ 113 | static getFunctionVersionName(versions, functionName) { 114 | return _.find(_.keys(versions), version => _.startsWith(version, `${functionName}LambdaVersion`)); 115 | } 116 | 117 | /** 118 | * @param {object} aliases 119 | * @param {string} functionName 120 | * @returns {string} 121 | */ 122 | static getAliasVersionName(aliases, functionName) { 123 | return _.find(_.keys(aliases), alias => _.startsWith(alias, `${functionName}Alias`)); 124 | } 125 | } 126 | 127 | module.exports = Utils; 128 | -------------------------------------------------------------------------------- /lib/validate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Perform validation. 4 | */ 5 | 6 | const BbPromise = require('bluebird'); 7 | const SemVer = require('semver'); 8 | 9 | module.exports = { 10 | 11 | validate() { 12 | 13 | // Check required serverless version 14 | if (SemVer.gt('1.12.0', this.serverless.getVersion())) { 15 | return BbPromise.reject(new this.serverless.classes.Error('Serverless verion must be >= 1.12.0')); 16 | } 17 | 18 | // Set configuration 19 | this._stage = this._provider.getStage(); 20 | this._masterAlias = this._options.masterAlias || this._stage; 21 | this._alias = this._options.alias || this._masterAlias; 22 | this._stackName = this._provider.naming.getStackName(); 23 | this._retain = this._options.retain || false; 24 | 25 | // Make alias available as ${self:provider.alias} 26 | this._serverless.service.provider.alias = this._alias; 27 | 28 | // Set SERVERLESS_ALIAS environment variable to let other plugins access it during the build 29 | process.env.SERVERLESS_ALIAS = this._alias; 30 | 31 | // Parse and check plugin options 32 | if (this._options['alias-resources']) { 33 | this._aliasResources = true; 34 | } 35 | 36 | this._validated = true; 37 | 38 | return BbPromise.resolve(); 39 | 40 | } 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-aws-alias", 3 | "version": "1.8.0", 4 | "description": "Serverless plugin to support AWS function aliases", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/serverless-heaven/serverless-aws-alias" 9 | }, 10 | "scripts": { 11 | "test": "nyc ./node_modules/mocha/bin/_mocha \"test/**/*.js\" -R spec --recursive", 12 | "eslint": "node node_modules/eslint/bin/eslint.js --ext .js lib" 13 | }, 14 | "author": "Frank Schmid ", 15 | "license": "MIT", 16 | "keywords": [ 17 | "serverless", 18 | "plugin", 19 | "1.0", 20 | "aws", 21 | "lambda", 22 | "alias", 23 | "aliases" 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/serverless-heaven/serverless-aws-alias/issues" 27 | }, 28 | "homepage": "https://github.com/serverless-heaven/serverless-aws-alias#readme", 29 | "nyc": { 30 | "exclude": [ 31 | "test/**/*.*" 32 | ], 33 | "reporter": [ 34 | "lcov", 35 | "text-summary" 36 | ], 37 | "report-dir": "./coverage" 38 | }, 39 | "dependencies": { 40 | "bluebird": "^3.7.0", 41 | "chalk": "^2.4.2", 42 | "lodash": "^4.17.15", 43 | "moment": "^2.24.0", 44 | "os": "^0.1.1", 45 | "semver": "^6.3.0" 46 | }, 47 | "devDependencies": { 48 | "chai": "^4.2.0", 49 | "chai-as-promised": "^7.1.1", 50 | "chai-subset": "^1.6.0", 51 | "coveralls": "^3.0.6", 52 | "eslint": "^6.5.1", 53 | "eslint-plugin-import": "^2.18.2", 54 | "eslint-plugin-lodash": "^6.0.0", 55 | "eslint-plugin-promise": "^4.2.1", 56 | "get-installed-path": "^4.0.8", 57 | "mocha": "^6.2.1", 58 | "nyc": "^14.1.1", 59 | "serverless": "^1.53.0", 60 | "sinon": "^7.5.0", 61 | "sinon-chai": "^3.3.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/aliasRestructureStack.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Unit tests for stack restructuring 4 | */ 5 | 6 | const { getInstalledPathSync } = require('get-installed-path'); 7 | const _ = require('lodash'); 8 | const BbPromise = require('bluebird'); 9 | const chai = require('chai'); 10 | const sinon = require('sinon'); 11 | const AWSAlias = require('../index'); 12 | 13 | const serverlessPath = getInstalledPathSync('serverless', { local: true }); 14 | const AwsProvider = require(`${serverlessPath}/lib/plugins/aws/provider/awsProvider`); 15 | const Serverless = require(`${serverlessPath}/lib/Serverless`); 16 | 17 | chai.use(require('chai-as-promised')); 18 | chai.use(require('sinon-chai')); 19 | const expect = chai.expect; 20 | 21 | describe('aliasRestructureStack', () => { 22 | let serverless; 23 | let options; 24 | let awsAlias; 25 | // Sinon and stubs for SLS CF access 26 | let sandbox; 27 | let logStub; 28 | 29 | before(() => { 30 | sandbox = sinon.createSandbox(); 31 | }); 32 | 33 | beforeEach(() => { 34 | serverless = new Serverless(); 35 | options = { 36 | alias: 'myAlias', 37 | stage: 'myStage', 38 | region: 'us-east-1', 39 | }; 40 | serverless.setProvider('aws', new AwsProvider(serverless, options)); 41 | serverless.cli = new serverless.classes.CLI(serverless); 42 | serverless.service.service = 'testService'; 43 | serverless.service.provider.compiledCloudFormationAliasTemplate = { 44 | Resources: {}, 45 | Outputs: {} 46 | }; 47 | serverless.service.provider.compiledCloudFormationTemplate = _.cloneDeep(require('./data/sls-stack-1.json')); 48 | awsAlias = new AWSAlias(serverless, options); 49 | 50 | // Disable logging 51 | logStub = sandbox.stub(serverless.cli, 'log'); 52 | logStub.returns(); 53 | }); 54 | 55 | afterEach(() => { 56 | sandbox.restore(); 57 | }); 58 | 59 | describe('#addMasterAliasName', () => { 60 | it('should add the master alias name as output from command line option', () => { 61 | serverless.service.provider.compiledCloudFormationTemplate = _.cloneDeep({ 62 | Resources: {}, 63 | Outputs: {} 64 | }); 65 | awsAlias._masterAlias = 'master' 66 | return expect(awsAlias.addMasterAliasName()).to.be.fulfilled 67 | .then(() => 68 | expect(serverless.service.provider.compiledCloudFormationTemplate.Outputs.MasterAliasName.Value) 69 | .to.equal('master') 70 | ); 71 | }); 72 | 73 | it('should add the master alias name as output from existing stack', () => { 74 | const masterAliasStackOutput = { 75 | MasterAliasName: { 76 | Description: 'Master Alias name (serverless-aws-alias plugin)', 77 | Value: 'master', 78 | Export: { 79 | Name: 'sls-test-project-dev-master' 80 | } 81 | } 82 | }; 83 | const currentTemplate = { 84 | Outputs: masterAliasStackOutput 85 | }; 86 | serverless.service.provider.compiledCloudFormationTemplate = _.cloneDeep({ 87 | Resources: {}, 88 | Outputs: {} 89 | }); 90 | return expect(awsAlias.addMasterAliasName(currentTemplate)).to.be.fulfilled 91 | .then(() => 92 | expect(serverless.service.provider.compiledCloudFormationTemplate.Outputs.MasterAliasName.Value) 93 | .to.equal('master') 94 | ); 95 | }); 96 | }); 97 | 98 | describe('#aliasFinalize()', () => { 99 | it('should stringify flags', () => { 100 | serverless.service.provider.compiledCloudFormationAliasTemplate = { 101 | Resources: {}, 102 | Outputs: { 103 | AliasFlags: { 104 | Value: { 105 | flag1: true, 106 | flag2: 0 107 | } 108 | } 109 | } 110 | }; 111 | 112 | return expect(awsAlias.aliasFinalize()).to.be.fulfilled 113 | .then(() => 114 | expect(serverless.service.provider.compiledCloudFormationAliasTemplate.Outputs.AliasFlags.Value) 115 | .to.equal('{"flag1":true,"flag2":0}') 116 | ); 117 | }); 118 | }); 119 | 120 | describe('#aliasRestructureStack()', () => { 121 | it('should abort if no master alias has been deployed', () => { 122 | awsAlias._alias = 'myAlias'; 123 | return expect(() => awsAlias.aliasRestructureStack({}, [], {})).to.throw(serverless.classes.Error); 124 | }); 125 | 126 | it('should propagate templates through all stack operations', () => { 127 | const addMasterAliasNameSpy = sandbox.spy(awsAlias, 'addMasterAliasName'); 128 | const aliasInitSpy = sandbox.spy(awsAlias, 'aliasInit'); 129 | const aliasHandleUserResourcesSpy = sandbox.spy(awsAlias, 'aliasHandleUserResources'); 130 | const aliasHandleLambdaRoleSpy = sandbox.spy(awsAlias, 'aliasHandleLambdaRole'); 131 | const aliasHandleFunctionsSpy = sandbox.spy(awsAlias, 'aliasHandleFunctions'); 132 | const aliasHandleApiGatewaySpy = sandbox.spy(awsAlias, 'aliasHandleApiGateway'); 133 | const aliasHandleEventsSpy = sandbox.spy(awsAlias, 'aliasHandleEvents'); 134 | const aliasHandleCWEventsSpy = sandbox.spy(awsAlias, 'aliasHandleCWEvents'); 135 | const aliasHandleSNSEventsSpy = sandbox.spy(awsAlias, 'aliasHandleSNSEvents'); 136 | const aliasFinalizeSpy = sandbox.spy(awsAlias, 'aliasFinalize'); 137 | 138 | const currentTemplate = _.cloneDeep(require('./data/sls-stack-2.json')); 139 | const aliasTemplate = _.cloneDeep(require('./data/alias-stack-1.json')); 140 | const currentAliasStackTemplate = {}; 141 | 142 | return expect(awsAlias.aliasRestructureStack(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate)) 143 | .to.be.fulfilled 144 | .then(() => BbPromise.all([ 145 | expect(addMasterAliasNameSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate), 146 | expect(aliasInitSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate), 147 | expect(aliasHandleUserResourcesSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate), 148 | expect(aliasHandleLambdaRoleSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate), 149 | expect(aliasHandleFunctionsSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate), 150 | expect(aliasHandleApiGatewaySpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate), 151 | expect(aliasHandleEventsSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate), 152 | expect(aliasHandleCWEventsSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate), 153 | expect(aliasHandleSNSEventsSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate), 154 | expect(aliasFinalizeSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate), 155 | ])); 156 | }); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /test/configureAliasStack.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Unit tests for configureAliasStack. 4 | */ 5 | 6 | const { getInstalledPathSync } = require('get-installed-path'); 7 | const _ = require('lodash'); 8 | const BbPromise = require('bluebird'); 9 | const chai = require('chai'); 10 | const sinon = require('sinon'); 11 | const AWSAlias = require('../index'); 12 | 13 | const serverlessPath = getInstalledPathSync('serverless', { local: true }); 14 | const AwsProvider = require(`${serverlessPath}/lib/plugins/aws/provider/awsProvider`); 15 | const Serverless = require(`${serverlessPath}/lib/Serverless`); 16 | 17 | chai.use(require('chai-as-promised')); 18 | chai.use(require('sinon-chai')); 19 | const expect = chai.expect; 20 | 21 | describe('configureAliasStack', () => { 22 | let serverless; 23 | let options; 24 | let awsAlias; 25 | // Sinon and stubs for SLS CF access 26 | let sandbox; 27 | let logStub; 28 | 29 | before(() => { 30 | sandbox = sinon.createSandbox(); 31 | }); 32 | 33 | beforeEach(() => { 34 | options = { 35 | alias: 'myAlias', 36 | stage: 'myStage', 37 | region: 'us-east-1', 38 | }; 39 | serverless = new Serverless(options); 40 | serverless.setProvider('aws', new AwsProvider(serverless)); 41 | serverless.cli = new serverless.classes.CLI(serverless); 42 | serverless.service.service = 'testService'; 43 | serverless.service.provider.compiledCloudFormationAliasTemplate = {}; 44 | awsAlias = new AWSAlias(serverless, options); 45 | 46 | // Disable logging 47 | logStub = sandbox.stub(serverless.cli, 'log'); 48 | logStub.returns(); 49 | }); 50 | 51 | afterEach(() => { 52 | sandbox.restore(); 53 | }); 54 | 55 | describe('#configureAliasStack()', () => { 56 | let readFileSyncStub; 57 | let stack1; 58 | 59 | beforeEach(() => { 60 | readFileSyncStub = sandbox.stub(serverless.utils, 'readFileSync'); 61 | stack1 = _.cloneDeep(require('./data/sls-stack-1.json')); 62 | }); 63 | 64 | it('should set alias reference and properties to CF templates', () => { 65 | readFileSyncStub.returns(require('../lib/alias-cloudformation-template.json')); 66 | serverless.service.provider.compiledCloudFormationTemplate = stack1; 67 | const cfTemplate = serverless.service.provider.compiledCloudFormationTemplate; 68 | 69 | return expect(awsAlias.validate()).to.be.fulfilled 70 | .then(() => expect(awsAlias.configureAliasStack()).to.be.fulfilled) 71 | .then(() => BbPromise.all([ 72 | expect(cfTemplate).to.have.nested.property('Outputs.ServerlessAliasReference.Value', 'REFERENCE'), 73 | expect(cfTemplate).to.have.nested.property('Outputs.ServerlessAliasReference.Export.Name', 'testService-myStage-ServerlessAliasReference'), 74 | expect(serverless.service.provider.compiledCloudFormationAliasTemplate) 75 | .to.have.property('Description') 76 | .that.matches(/Alias stack for .* \(.*\)/), 77 | expect(serverless.service.provider.compiledCloudFormationAliasTemplate) 78 | .to.have.nested.property('Outputs.ServerlessAliasName.Value', 'myAlias'), 79 | ])); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/createAliasStack.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Unit tests for createAliasStack.. 4 | */ 5 | 6 | const { getInstalledPathSync } = require('get-installed-path'); 7 | const BbPromise = require('bluebird'); 8 | const chai = require('chai'); 9 | const sinon = require('sinon'); 10 | const path = require('path'); 11 | const AWSAlias = require('../index'); 12 | 13 | const serverlessPath = getInstalledPathSync('serverless', { local: true }); 14 | const AwsProvider = require(`${serverlessPath}/lib/plugins/aws/provider/awsProvider`); 15 | const Serverless = require(`${serverlessPath}/lib/Serverless`); 16 | 17 | chai.use(require('chai-as-promised')); 18 | chai.use(require('sinon-chai')); 19 | const expect = chai.expect; 20 | 21 | describe('createAliasStack', () => { 22 | let serverless; 23 | let options; 24 | let awsAlias; 25 | // Sinon and stubs for SLS CF access 26 | let sandbox; 27 | let providerRequestStub; 28 | let monitorStackStub; 29 | let logStub; 30 | 31 | before(() => { 32 | sandbox = sinon.createSandbox(); 33 | }); 34 | 35 | beforeEach(() => { 36 | serverless = new Serverless(); 37 | options = { 38 | alias: 'myAlias', 39 | stage: 'myStage', 40 | region: 'us-east-1', 41 | }; 42 | serverless.setProvider('aws', new AwsProvider(serverless, options)); 43 | serverless.cli = new serverless.classes.CLI(serverless); 44 | serverless.service.service = 'testService'; 45 | serverless.service.provider.compiledCloudFormationAliasTemplate = {}; 46 | awsAlias = new AWSAlias(serverless, options); 47 | providerRequestStub = sandbox.stub(awsAlias._provider, 'request'); 48 | monitorStackStub = sandbox.stub(awsAlias, 'monitorStack'); 49 | logStub = sandbox.stub(serverless.cli, 'log'); 50 | 51 | logStub.returns(); 52 | }); 53 | 54 | afterEach(() => { 55 | sandbox.restore(); 56 | }); 57 | 58 | describe('#createAlias()', () => { 59 | it('Should call CF with correct default parameters', () => { 60 | const expectedCFData = { 61 | StackName: 'testService-myStage-myAlias', 62 | Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], 63 | OnFailure: 'DELETE', 64 | Parameters: [], 65 | Tags: [ 66 | { Key: 'STAGE', Value: 'myStage' }, 67 | { Key: 'ALIAS', Value: 'myAlias' } 68 | ], 69 | TemplateBody: '{}' 70 | }; 71 | const requestResult = { 72 | status: 'ok' 73 | }; 74 | providerRequestStub.returns(BbPromise.resolve(requestResult)); 75 | monitorStackStub.returns(BbPromise.resolve()); 76 | 77 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = {}; 78 | 79 | return expect(awsAlias.validate()).to.be.fulfilled 80 | .then(() => expect(awsAlias.createAlias()).to.be.fulfilled) 81 | .then(() => BbPromise.all([ 82 | expect(providerRequestStub).to.have.been.calledOnce, 83 | expect(monitorStackStub).to.have.been.calledOnce, 84 | expect(providerRequestStub).to.have.been 85 | .calledWithExactly('CloudFormation', 'createStack', expectedCFData, 'myStage', 'us-east-1'), 86 | expect(monitorStackStub).to.have.been 87 | .calledWithExactly('create', requestResult) 88 | ])); 89 | }); 90 | 91 | it('should set stack tags', () => { 92 | const expectedCFData = { 93 | StackName: 'testService-myStage-myAlias', 94 | Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], 95 | OnFailure: 'DELETE', 96 | Parameters: [], 97 | Tags: [ 98 | { Key: 'STAGE', Value: 'myStage' }, 99 | { Key: 'ALIAS', Value: 'myAlias' }, 100 | { Key: 'tag1', Value: 'application'}, 101 | { Key: 'tag2', Value: 'component' } 102 | ], 103 | TemplateBody: '{}' 104 | }; 105 | providerRequestStub.returns(BbPromise.resolve("done")); 106 | monitorStackStub.returns(BbPromise.resolve()); 107 | 108 | serverless.service.provider.stackTags = { 109 | tag1: 'application', 110 | tag2: 'component' 111 | }; 112 | 113 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = {}; 114 | 115 | return expect(awsAlias.validate()).to.be.fulfilled 116 | .then(() => expect(awsAlias.createAlias()).to.be.fulfilled) 117 | .then(() => BbPromise.all([ 118 | expect(providerRequestStub).to.have.been.calledOnce, 119 | expect(providerRequestStub).to.have.been 120 | .calledWithExactly('CloudFormation', 'createStack', expectedCFData, 'myStage', 'us-east-1'), 121 | ])); 122 | }); 123 | 124 | it('should reject with CF error', () => { 125 | providerRequestStub.returns(BbPromise.reject(new Error('CF failed'))); 126 | monitorStackStub.returns(BbPromise.resolve()); 127 | 128 | return expect(awsAlias.createAlias()).to.be.rejectedWith('CF failed') 129 | .then(() => expect(providerRequestStub).to.have.been.calledOnce); 130 | }); 131 | }); 132 | 133 | describe('#createAliasStack()', () => { 134 | let writeAliasTemplateToDiskStub; 135 | let checkAliasStackStub; 136 | 137 | beforeEach(() => { 138 | writeAliasTemplateToDiskStub = sandbox.stub(awsAlias, 'writeAliasTemplateToDisk'); 139 | checkAliasStackStub = sandbox.stub(awsAlias, 'checkAliasStack'); 140 | }); 141 | 142 | it('should fail with invalid service name', () => { 143 | writeAliasTemplateToDiskStub.returns(BbPromise.resolve()); 144 | checkAliasStackStub.returns(BbPromise.resolve()); 145 | 146 | serverless.service.service = 'testSer?vice'; 147 | return expect(() => awsAlias.createAliasStack()).to.throw('is not valid'); 148 | }); 149 | 150 | it('should fail with invalid alias name', () => { 151 | writeAliasTemplateToDiskStub.returns(BbPromise.resolve()); 152 | checkAliasStackStub.returns(BbPromise.resolve()); 153 | 154 | awsAlias._alias = 'ali!as'; 155 | return expect(() => awsAlias.createAliasStack()).to.throw('is not valid'); 156 | }); 157 | 158 | it('should fail with too long stack name', () => { 159 | writeAliasTemplateToDiskStub.returns(BbPromise.resolve()); 160 | checkAliasStackStub.returns(BbPromise.resolve()); 161 | 162 | awsAlias._alias = Array(513).join('x'); 163 | return expect(() => awsAlias.createAliasStack()).to.throw('is not valid'); 164 | }); 165 | 166 | it('should save template and create stack', () => { 167 | writeAliasTemplateToDiskStub.returns(BbPromise.resolve()); 168 | checkAliasStackStub.returns(BbPromise.resolve()); 169 | 170 | return expect(awsAlias.createAliasStack()).to.be.fulfilled 171 | .then(() => BbPromise.all([ 172 | expect(writeAliasTemplateToDiskStub).to.have.been.calledOnce, 173 | expect(checkAliasStackStub).to.have.been.calledOnce, 174 | expect(writeAliasTemplateToDiskStub).to.have.been.calledBefore(checkAliasStackStub), 175 | ])); 176 | }); 177 | }); 178 | 179 | describe('#checkAliasStack()', () => { 180 | let createAliasStub; 181 | 182 | beforeEach(() => { 183 | createAliasStub = sandbox.stub(awsAlias, 'createAlias'); 184 | }); 185 | 186 | it('should do nothing with --noDeploy', () => { 187 | providerRequestStub.returns(BbPromise.resolve()); 188 | createAliasStub.returns(BbPromise.resolve()); 189 | 190 | awsAlias._options.noDeploy = true; 191 | 192 | return expect(awsAlias.checkAliasStack()).to.be.fulfilled 193 | .then(() => BbPromise.all([ 194 | expect(providerRequestStub).to.not.have.been.called, 195 | expect(createAliasStub).to.not.have.been.called, 196 | ])); 197 | }); 198 | 199 | it('Should call CF describeStackResources and resolve if stack exists', () => { 200 | const expectedCFData = { StackName: 'testService-dev-myAlias' }; 201 | providerRequestStub.returns(BbPromise.resolve()); 202 | monitorStackStub.returns(BbPromise.resolve()); 203 | 204 | awsAlias._aliasStackName = 'testService-dev-myAlias'; 205 | 206 | return expect(awsAlias.checkAliasStack()).to.eventually.equal('alreadyCreated') 207 | .then(() => BbPromise.all([ 208 | expect(providerRequestStub).to.have.been.calledOnce, 209 | expect(createAliasStub).to.not.have.been.called, 210 | expect(providerRequestStub).to.have.been 211 | .calledWithExactly('CloudFormation', 'describeStackResources', expectedCFData, 'myStage', 'us-east-1'), 212 | ])); 213 | }); 214 | 215 | it('Should create stack if it does not exist', () => { 216 | providerRequestStub.returns(BbPromise.reject(new Error('stack does not exist'))); 217 | monitorStackStub.returns(BbPromise.resolve()); 218 | 219 | awsAlias._aliasStackName = 'testService-dev-myAlias'; 220 | awsAlias._createLater = false; 221 | 222 | return expect(awsAlias.checkAliasStack()).to.be.fulfilled 223 | .then(() => BbPromise.all([ 224 | expect(createAliasStub).to.have.been.calledOnce, 225 | expect(awsAlias._createLater).to.be.false 226 | ])); 227 | }); 228 | 229 | it('Should defer stack creation if a deployment bucket has been set', () => { 230 | providerRequestStub.returns(BbPromise.reject(new Error('stack does not exist'))); 231 | monitorStackStub.returns(BbPromise.resolve()); 232 | 233 | serverless.service.provider.deploymentBucket = 'myBucket'; 234 | awsAlias._aliasStackName = 'testService-dev-myAlias'; 235 | awsAlias._createLater = false; 236 | 237 | return expect(awsAlias.checkAliasStack()).to.be.fulfilled 238 | .then(() => BbPromise.all([ 239 | expect(createAliasStub).to.not.have.been.called, 240 | expect(awsAlias._createLater).to.be.true 241 | ])); 242 | }); 243 | 244 | it('should throw on unknown CF error', () => { 245 | providerRequestStub.returns(BbPromise.reject(new Error('invalid CF operation'))); 246 | createAliasStub.returns(BbPromise.resolve()); 247 | 248 | return expect(awsAlias.checkAliasStack()).to.be.rejectedWith('invalid CF operation'); 249 | }); 250 | }); 251 | 252 | describe('#writeAliasTemplateToDisk()', () => { 253 | let writeFileSyncStub; 254 | 255 | beforeEach(() => { 256 | writeFileSyncStub = sandbox.stub(serverless.utils, 'writeFileSync'); 257 | }); 258 | 259 | it('should do nothing if a deployment bucket has been set', () => { 260 | writeFileSyncStub.returns(); 261 | 262 | serverless.service.provider.deploymentBucket = 'myBucket'; 263 | serverless.config.servicePath = 'path-to-service'; 264 | 265 | return expect(awsAlias.writeAliasTemplateToDisk()).to.be.fulfilled 266 | .then(() => expect(writeFileSyncStub).to.not.have.been.called); 267 | }); 268 | 269 | it('should write the alias template', () => { 270 | const expectedPath = path.join('path-to-service', '.serverless', 'cloudformation-template-create-alias-stack.json'); 271 | const template = { stack: 'mystacktemplate' }; 272 | writeFileSyncStub.returns(); 273 | 274 | serverless.config.servicePath = 'path-to-service'; 275 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = template; 276 | 277 | return expect(awsAlias.writeAliasTemplateToDisk()).to.be.fulfilled 278 | .then(() => BbPromise.all([ 279 | expect(writeFileSyncStub).to.have.been.calledOnce, 280 | expect(writeFileSyncStub).to.have.been.calledWithExactly(expectedPath, template) 281 | ])); 282 | }); 283 | }); 284 | }); 285 | -------------------------------------------------------------------------------- /test/data/alias-stack-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Alias stack for sls-test-project-dev (myAlias)", 4 | "Resources": { 5 | "ServerlessAliasLogGroup": { 6 | "Type": "AWS::Logs::LogGroup", 7 | "Properties": { 8 | "LogGroupName": "/serverless/sls-test-project-dev-myAlias", 9 | "RetentionInDays": 7 10 | } 11 | }, 12 | "Testfct1WithSuffixAlias": { 13 | "Type": "AWS::Lambda::Alias", 14 | "Properties": { 15 | "Description": "Echo function echoes alias", 16 | "FunctionName": { 17 | "Fn::ImportValue": "sls-test-project-dev-Testfct1WithSuffix-LambdaFunctionArn" 18 | }, 19 | "FunctionVersion": { 20 | "Fn::GetAtt": [ 21 | "Testfct1WithSuffixLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU", 22 | "Version" 23 | ] 24 | }, 25 | "Name": "myAlias" 26 | }, 27 | "DependsOn": [ 28 | "Testfct1WithSuffixLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU" 29 | ] 30 | }, 31 | "Testfct1Alias": { 32 | "Type": "AWS::Lambda::Alias", 33 | "Properties": { 34 | "Description": "Echo function echoes alias", 35 | "FunctionName": { 36 | "Fn::ImportValue": "sls-test-project-dev-Testfct1-LambdaFunctionArn" 37 | }, 38 | "FunctionVersion": { 39 | "Fn::GetAtt": [ 40 | "Testfct1LambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU", 41 | "Version" 42 | ] 43 | }, 44 | "Name": "myAlias" 45 | }, 46 | "DependsOn": [ 47 | "Testfct1LambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU" 48 | ] 49 | }, 50 | "WarmUpPluginAlias": { 51 | "Type": "AWS::Lambda::Alias", 52 | "Properties": { 53 | "Description": "Serverless WarmUP Plugin", 54 | "FunctionName": { 55 | "Fn::ImportValue": "sls-test-project-dev-WarmUpPlugin-LambdaFunctionArn" 56 | }, 57 | "FunctionVersion": { 58 | "Fn::GetAtt": [ 59 | "WarmUpPluginLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU", 60 | "Version" 61 | ] 62 | }, 63 | "Name": "myAlias" 64 | }, 65 | "DependsOn": [ 66 | "WarmUpPluginLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU" 67 | ] 68 | }, 69 | "Testfct1WithSuffixLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU": { 70 | "Type": "AWS::Lambda::Version", 71 | "DeletionPolicy": "Delete", 72 | "Properties": { 73 | "FunctionName": { 74 | "Fn::ImportValue": "sls-test-project-dev-Testfct1WithSuffix-LambdaFunctionArn" 75 | }, 76 | "CodeSha256": "Wh5jTkiTR67+V05RPWQIlzPI25WiPbdHDYNgbtAMneU=", 77 | "Description": "Echo function echoes alias" 78 | } 79 | }, 80 | "Testfct1LambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU": { 81 | "Type": "AWS::Lambda::Version", 82 | "DeletionPolicy": "Delete", 83 | "Properties": { 84 | "FunctionName": { 85 | "Fn::ImportValue": "sls-test-project-dev-Testfct1-LambdaFunctionArn" 86 | }, 87 | "CodeSha256": "Wh5jTkiTR67+V05RPWQIlzPI25WiPbdHDYNgbtAMneU=", 88 | "Description": "Echo function echoes alias" 89 | } 90 | }, 91 | "WarmUpPluginLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU": { 92 | "Type": "AWS::Lambda::Version", 93 | "DeletionPolicy": "Delete", 94 | "Properties": { 95 | "FunctionName": { 96 | "Fn::ImportValue": "sls-test-project-dev-WarmUpPlugin-LambdaFunctionArn" 97 | }, 98 | "CodeSha256": "Wh5jTkiTR67+V05RPWQIlzPI25WiPbdHDYNgbtAMneU=", 99 | "Description": "Serverless WarmUP Plugin" 100 | } 101 | }, 102 | "ApiGatewayDeployment1494367071211": { 103 | "Type": "AWS::ApiGateway::Deployment", 104 | "Properties": { 105 | "RestApiId": { 106 | "Fn::ImportValue": "sls-test-project-dev-ApiGatewayRestApi" 107 | } 108 | }, 109 | "DependsOn": [] 110 | }, 111 | "ApiGatewayStage": { 112 | "Type": "AWS::ApiGateway::Stage", 113 | "Properties": { 114 | "StageName": "myAlias", 115 | "DeploymentId": { 116 | "Ref": "ApiGatewayDeployment1494367071211" 117 | }, 118 | "RestApiId": { 119 | "Fn::ImportValue": "sls-test-project-dev-ApiGatewayRestApi" 120 | }, 121 | "Variables": { 122 | "SERVERLESS_ALIAS": "myAlias", 123 | "SERVERLESS_STAGE": "dev" 124 | } 125 | }, 126 | "DependsOn": [ 127 | "ApiGatewayDeployment1494367071211" 128 | ] 129 | }, 130 | "Testfct1WithSuffixLambdaPermissionApiGateway": { 131 | "Type": "AWS::Lambda::Permission", 132 | "Properties": { 133 | "FunctionName": { 134 | "Ref": "Testfct1WithSuffixAlias" 135 | }, 136 | "Action": "lambda:InvokeFunction", 137 | "Principal": "apigateway.amazonaws.com", 138 | "SourceArn": { 139 | "Fn::Join": [ 140 | "", 141 | [ 142 | "arn:aws:execute-api:", 143 | { 144 | "Ref": "AWS::Region" 145 | }, 146 | ":", 147 | { 148 | "Ref": "AWS::AccountId" 149 | }, 150 | ":", 151 | { 152 | "Fn::ImportValue": "sls-test-project-dev-ApiGatewayRestApi" 153 | }, 154 | "/*/*" 155 | ] 156 | ] 157 | } 158 | }, 159 | "DependsOn": [ 160 | "Testfct1WithSuffixLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU", 161 | "Testfct1WithSuffixAlias" 162 | ] 163 | }, 164 | "Testfct1LambdaPermissionApiGateway": { 165 | "Type": "AWS::Lambda::Permission", 166 | "Properties": { 167 | "FunctionName": { 168 | "Ref": "Testfct1Alias" 169 | }, 170 | "Action": "lambda:InvokeFunction", 171 | "Principal": "apigateway.amazonaws.com", 172 | "SourceArn": { 173 | "Fn::Join": [ 174 | "", 175 | [ 176 | "arn:aws:execute-api:", 177 | { 178 | "Ref": "AWS::Region" 179 | }, 180 | ":", 181 | { 182 | "Ref": "AWS::AccountId" 183 | }, 184 | ":", 185 | { 186 | "Fn::ImportValue": "sls-test-project-dev-ApiGatewayRestApi" 187 | }, 188 | "/*/*" 189 | ] 190 | ] 191 | } 192 | }, 193 | "DependsOn": [ 194 | "Testfct1LambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU", 195 | "Testfct1Alias" 196 | ] 197 | }, 198 | "WarmUpPluginEventsRuleSchedule1": { 199 | "Type": "AWS::Events::Rule", 200 | "Properties": { 201 | "ScheduleExpression": "rate(5 minutes)", 202 | "State": "ENABLED", 203 | "Targets": [ 204 | { 205 | "Arn": { 206 | "Ref": "WarmUpPluginAlias" 207 | }, 208 | "Id": "warmUpPluginSchedule" 209 | } 210 | ] 211 | }, 212 | "DependsOn": [ 213 | "WarmUpPluginAlias" 214 | ] 215 | }, 216 | "WarmUpPluginLambdaPermissionEventsRuleSchedule1": { 217 | "Type": "AWS::Lambda::Permission", 218 | "Properties": { 219 | "FunctionName": { 220 | "Ref": "WarmUpPluginAlias" 221 | }, 222 | "Action": "lambda:InvokeFunction", 223 | "Principal": "events.amazonaws.com", 224 | "SourceArn": { 225 | "Fn::GetAtt": [ 226 | "WarmUpPluginEventsRuleSchedule1", 227 | "Arn" 228 | ] 229 | } 230 | }, 231 | "DependsOn": [ 232 | "WarmUpPluginAlias" 233 | ] 234 | } 235 | }, 236 | "Outputs": { 237 | "ServerlessAliasName": { 238 | "Description": "Alias the stack represents.", 239 | "Value": "myAlias" 240 | }, 241 | "ServerlessAliasLogGroup": { 242 | "Description": "Log group for alias.", 243 | "Value": { 244 | "Ref": "ServerlessAliasLogGroup" 245 | }, 246 | "Export": { 247 | "Name": "sls-test-project-dev-myAlias-LogGroup" 248 | } 249 | }, 250 | "AliasFlags": { 251 | "Description": "Alias flags.", 252 | "Value": "{\"hasRole\":false}" 253 | }, 254 | "AliasResources": { 255 | "Description": "Custom resource references", 256 | "Value": "[]" 257 | }, 258 | "AliasOutputs": { 259 | "Description": "Custom output references", 260 | "Value": "[]" 261 | }, 262 | "ServerlessAliasReference": { 263 | "Description": "Alias stack reference.", 264 | "Value": { 265 | "Fn::ImportValue": "sls-test-project-dev-ServerlessAliasReference" 266 | } 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /test/data/alias-stack-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Alias stack for sls-test-project-dev (myAlias)", 4 | "Resources": { 5 | "ServerlessAliasLogGroup": { 6 | "Type": "AWS::Logs::LogGroup", 7 | "Properties": { 8 | "LogGroupName": "/serverless/sls-test-project-dev-myAlias", 9 | "RetentionInDays": 7 10 | } 11 | }, 12 | "Testfct1Alias": { 13 | "Type": "AWS::Lambda::Alias", 14 | "Properties": { 15 | "Description": "Echo function echoes alias", 16 | "FunctionName": { 17 | "Fn::ImportValue": "sls-test-project-dev-Testfct1-LambdaFunctionArn" 18 | }, 19 | "FunctionVersion": { 20 | "Fn::GetAtt": [ 21 | "Testfct1LambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU", 22 | "Version" 23 | ] 24 | }, 25 | "Name": "myAlias" 26 | }, 27 | "DependsOn": [ 28 | "Testfct1LambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU" 29 | ] 30 | }, 31 | "WarmUpPluginAlias": { 32 | "Type": "AWS::Lambda::Alias", 33 | "Properties": { 34 | "Description": "Serverless WarmUP Plugin", 35 | "FunctionName": { 36 | "Fn::ImportValue": "sls-test-project-dev-WarmUpPlugin-LambdaFunctionArn" 37 | }, 38 | "FunctionVersion": { 39 | "Fn::GetAtt": [ 40 | "WarmUpPluginLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU", 41 | "Version" 42 | ] 43 | }, 44 | "Name": "myAlias" 45 | }, 46 | "DependsOn": [ 47 | "WarmUpPluginLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU" 48 | ] 49 | }, 50 | "Testfct1LambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU": { 51 | "Type": "AWS::Lambda::Version", 52 | "DeletionPolicy": "Delete", 53 | "Properties": { 54 | "FunctionName": { 55 | "Fn::ImportValue": "sls-test-project-dev-Testfct1-LambdaFunctionArn" 56 | }, 57 | "CodeSha256": "Wh5jTkiTR67+V05RPWQIlzPI25WiPbdHDYNgbtAMneU=", 58 | "Description": "Echo function echoes alias" 59 | } 60 | }, 61 | "WarmUpPluginLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU": { 62 | "Type": "AWS::Lambda::Version", 63 | "DeletionPolicy": "Delete", 64 | "Properties": { 65 | "FunctionName": { 66 | "Fn::ImportValue": "sls-test-project-dev-WarmUpPlugin-LambdaFunctionArn" 67 | }, 68 | "CodeSha256": "Wh5jTkiTR67+V05RPWQIlzPI25WiPbdHDYNgbtAMneU=", 69 | "Description": "Serverless WarmUP Plugin" 70 | } 71 | }, 72 | "ApiGatewayDeployment1494367071211": { 73 | "Type": "AWS::ApiGateway::Deployment", 74 | "Properties": { 75 | "RestApiId": { 76 | "Fn::ImportValue": "sls-test-project-dev-ApiGatewayRestApi" 77 | } 78 | }, 79 | "DependsOn": [] 80 | }, 81 | "ApiGatewayStage": { 82 | "Type": "AWS::ApiGateway::Stage", 83 | "Properties": { 84 | "StageName": "myAlias", 85 | "DeploymentId": { 86 | "Ref": "ApiGatewayDeployment1494367071211" 87 | }, 88 | "RestApiId": { 89 | "Fn::ImportValue": "sls-test-project-dev-ApiGatewayRestApi" 90 | }, 91 | "Variables": { 92 | "SERVERLESS_ALIAS": "myAlias", 93 | "SERVERLESS_STAGE": "dev" 94 | } 95 | }, 96 | "DependsOn": [ 97 | "ApiGatewayDeployment1494367071211" 98 | ] 99 | }, 100 | "Testfct1LambdaPermissionApiGateway": { 101 | "Type": "AWS::Lambda::Permission", 102 | "Properties": { 103 | "FunctionName": { 104 | "Ref": "Testfct1Alias" 105 | }, 106 | "Action": "lambda:InvokeFunction", 107 | "Principal": "apigateway.amazonaws.com", 108 | "SourceArn": { 109 | "Fn::Join": [ 110 | "", 111 | [ 112 | "arn:aws:execute-api:", 113 | { 114 | "Ref": "AWS::Region" 115 | }, 116 | ":", 117 | { 118 | "Ref": "AWS::AccountId" 119 | }, 120 | ":", 121 | { 122 | "Fn::ImportValue": "sls-test-project-dev-ApiGatewayRestApi" 123 | }, 124 | "/*/*" 125 | ] 126 | ] 127 | } 128 | }, 129 | "DependsOn": [ 130 | "Testfct1LambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU", 131 | "Testfct1Alias" 132 | ] 133 | }, 134 | "WarmUpPluginEventsRuleSchedule1": { 135 | "Type": "AWS::Events::Rule", 136 | "Properties": { 137 | "ScheduleExpression": "rate(5 minutes)", 138 | "State": "ENABLED", 139 | "Targets": [ 140 | { 141 | "Arn": { 142 | "Ref": "WarmUpPluginAlias" 143 | }, 144 | "Id": "warmUpPluginSchedule" 145 | } 146 | ] 147 | }, 148 | "DependsOn": [ 149 | "WarmUpPluginAlias" 150 | ] 151 | }, 152 | "WarmUpPluginLambdaPermissionEventsRuleSchedule1": { 153 | "Type": "AWS::Lambda::Permission", 154 | "Properties": { 155 | "FunctionName": { 156 | "Ref": "WarmUpPluginAlias" 157 | }, 158 | "Action": "lambda:InvokeFunction", 159 | "Principal": "events.amazonaws.com", 160 | "SourceArn": { 161 | "Fn::GetAtt": [ 162 | "WarmUpPluginEventsRuleSchedule1", 163 | "Arn" 164 | ] 165 | } 166 | }, 167 | "DependsOn": [ 168 | "WarmUpPluginAlias" 169 | ] 170 | } 171 | }, 172 | "Outputs": { 173 | "ServerlessAliasName": { 174 | "Description": "Alias the stack represents.", 175 | "Value": "myStage" 176 | }, 177 | "ServerlessAliasLogGroup": { 178 | "Description": "Log group for alias.", 179 | "Value": { 180 | "Ref": "ServerlessAliasLogGroup" 181 | }, 182 | "Export": { 183 | "Name": "sls-test-project-dev-myAlias-LogGroup" 184 | } 185 | }, 186 | "AliasFlags": { 187 | "Description": "Alias flags.", 188 | "Value": "{\"hasRole\":false}" 189 | }, 190 | "AliasResources": { 191 | "Description": "Custom resource references", 192 | "Value": "[]" 193 | }, 194 | "AliasOutputs": { 195 | "Description": "Custom output references", 196 | "Value": "[]" 197 | }, 198 | "ServerlessAliasReference": { 199 | "Description": "Alias stack reference.", 200 | "Value": { 201 | "Fn::ImportValue": "sls-test-project-dev-ServerlessAliasReference" 202 | } 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /test/data/sls-stack-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "The AWS CloudFormation template for this Serverless application", 4 | "Resources": { 5 | "ServerlessDeploymentBucket": { 6 | "Type": "AWS::S3::Bucket" 7 | }, 8 | "Testfct1LogGroup": { 9 | "Type": "AWS::Logs::LogGroup", 10 | "Properties": { 11 | "LogGroupName": "/aws/lambda/sls-test-project-dev-testfct1" 12 | } 13 | }, 14 | "IamRoleLambdaExecution": { 15 | "Type": "AWS::IAM::Role", 16 | "Properties": { 17 | "AssumeRolePolicyDocument": { 18 | "Version": "2012-10-17", 19 | "Statement": [ 20 | { 21 | "Effect": "Allow", 22 | "Principal": { 23 | "Service": [ 24 | "lambda.amazonaws.com" 25 | ] 26 | }, 27 | "Action": [ 28 | "sts:AssumeRole" 29 | ] 30 | } 31 | ] 32 | }, 33 | "Policies": [ 34 | { 35 | "PolicyName": { 36 | "Fn::Join": [ 37 | "-", 38 | [ 39 | "dev", 40 | "sls-test-project", 41 | "lambda" 42 | ] 43 | ] 44 | }, 45 | "PolicyDocument": { 46 | "Version": "2012-10-17", 47 | "Statement": [ 48 | { 49 | "Effect": "Allow", 50 | "Action": [ 51 | "logs:CreateLogStream" 52 | ], 53 | "Resource": [ 54 | { 55 | "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/sls-test-project-dev-testfct1:*" 56 | } 57 | ] 58 | }, 59 | { 60 | "Effect": "Allow", 61 | "Action": [ 62 | "logs:PutLogEvents" 63 | ], 64 | "Resource": [ 65 | { 66 | "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/sls-test-project-dev-testfct1:*:*" 67 | } 68 | ] 69 | }, 70 | { 71 | "Effect": "Allow", 72 | "Action": [ 73 | "dynamodb:*" 74 | ], 75 | "Resource": [ 76 | { 77 | "Fn::Join": [ 78 | "/", 79 | [ 80 | { 81 | "Fn::Join": [ 82 | ":", 83 | [ 84 | "arn:aws:dynamodb", 85 | { 86 | "Ref": "AWS::Region" 87 | }, 88 | { 89 | "Ref": "AWS::AccountId" 90 | }, 91 | "table" 92 | ] 93 | ] 94 | }, 95 | { 96 | "Ref": "TestDynamoDbTable" 97 | } 98 | ] 99 | ] 100 | } 101 | ] 102 | } 103 | ] 104 | } 105 | } 106 | ], 107 | "Path": "/", 108 | "RoleName": { 109 | "Fn::Join": [ 110 | "-", 111 | [ 112 | "sls-test-project", 113 | "dev", 114 | "us-east-1", 115 | "lambdaRole" 116 | ] 117 | ] 118 | }, 119 | "ManagedPolicyArns": [ 120 | "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" 121 | ] 122 | } 123 | }, 124 | "Testfct1LambdaFunction": { 125 | "Type": "AWS::Lambda::Function", 126 | "Properties": { 127 | "Code": { 128 | "S3Bucket": { 129 | "Ref": "ServerlessDeploymentBucket" 130 | }, 131 | "S3Key": "serverless/sls-test-project/dev/1494367071172-2017-05-09T21:57:51.172Z/sls-test-project.zip" 132 | }, 133 | "FunctionName": "sls-test-project-dev-testfct1", 134 | "Handler": "handlers/testfct1/handler.handle", 135 | "MemorySize": 512, 136 | "Role": { 137 | "Fn::GetAtt": [ 138 | "IamRoleLambdaExecution", 139 | "Arn" 140 | ] 141 | }, 142 | "Runtime": "nodejs4.3", 143 | "Timeout": 15, 144 | "Description": "Echo function echoes alias", 145 | "Environment": { 146 | "Variables": { 147 | "SERVERLESS_PROJECT_NAME": "sls-test-project", 148 | "SERVERLESS_PROJECT": "sls-test-project", 149 | "SERVERLESS_STAGE": "dev", 150 | "SERVERLESS_REGION": "us-east-1", 151 | "TEST_TABLE_NAME": { 152 | "Ref": "TestDynamoDbTable" 153 | } 154 | } 155 | }, 156 | "VpcConfig": { 157 | "SecurityGroupIds": [ 158 | { 159 | "Fn::ImportValue": "stashimi-dev-PrivateSG" 160 | } 161 | ], 162 | "SubnetIds": [ 163 | { 164 | "Fn::ImportValue": "stashimi-dev-PrivateSubnet1" 165 | }, 166 | { 167 | "Fn::ImportValue": "stashimi-dev-PrivateSubnet2" 168 | } 169 | ] 170 | } 171 | }, 172 | "DependsOn": [ 173 | "Testfct1LogGroup", 174 | "IamRoleLambdaExecution" 175 | ] 176 | }, 177 | "Testfct1LambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU": { 178 | "Type": "AWS::Lambda::Version", 179 | "DeletionPolicy": "Retain", 180 | "Properties": { 181 | "FunctionName": { 182 | "Ref": "Testfct1LambdaFunction" 183 | }, 184 | "CodeSha256": "Wh5jTkiTR67+V05RPWQIlzPI25WiPbdHDYNgbtAMneU=", 185 | "Description": "Echo function echoes alias" 186 | } 187 | }, 188 | "ApiGatewayRestApi": { 189 | "Type": "AWS::ApiGateway::RestApi", 190 | "Properties": { 191 | "Name": "dev-sls-test-project" 192 | } 193 | }, 194 | "ApiGatewayResourceFunc1": { 195 | "Type": "AWS::ApiGateway::Resource", 196 | "Properties": { 197 | "ParentId": { 198 | "Fn::GetAtt": [ 199 | "ApiGatewayRestApi", 200 | "RootResourceId" 201 | ] 202 | }, 203 | "PathPart": "func1", 204 | "RestApiId": { 205 | "Ref": "ApiGatewayRestApi" 206 | } 207 | } 208 | }, 209 | "ApiGatewayMethodFunc1Get": { 210 | "Type": "AWS::ApiGateway::Method", 211 | "Properties": { 212 | "HttpMethod": "GET", 213 | "RequestParameters": {}, 214 | "ResourceId": { 215 | "Ref": "ApiGatewayResourceFunc1" 216 | }, 217 | "RestApiId": { 218 | "Ref": "ApiGatewayRestApi" 219 | }, 220 | "AuthorizationType": "NONE", 221 | "Integration": { 222 | "IntegrationHttpMethod": "POST", 223 | "Type": "AWS_PROXY", 224 | "Uri": { 225 | "Fn::Join": [ 226 | "", 227 | [ 228 | "arn:aws:apigateway:", 229 | { 230 | "Ref": "AWS::Region" 231 | }, 232 | ":lambda:path/2015-03-31/functions/", 233 | { 234 | "Fn::GetAtt": [ 235 | "Testfct1LambdaFunction", 236 | "Arn" 237 | ] 238 | }, 239 | "/invocations" 240 | ] 241 | ] 242 | } 243 | }, 244 | "MethodResponses": [] 245 | } 246 | }, 247 | "ApiGatewayDeployment1494367071211": { 248 | "Type": "AWS::ApiGateway::Deployment", 249 | "Properties": { 250 | "RestApiId": { 251 | "Ref": "ApiGatewayRestApi" 252 | }, 253 | "StageName": "dev" 254 | }, 255 | "DependsOn": [ 256 | "ApiGatewayMethodFunc1Get" 257 | ] 258 | }, 259 | "Testfct1LambdaPermissionApiGateway": { 260 | "Type": "AWS::Lambda::Permission", 261 | "Properties": { 262 | "FunctionName": { 263 | "Fn::GetAtt": [ 264 | "Testfct1LambdaFunction", 265 | "Arn" 266 | ] 267 | }, 268 | "Action": "lambda:InvokeFunction", 269 | "Principal": "apigateway.amazonaws.com", 270 | "SourceArn": { 271 | "Fn::Join": [ 272 | "", 273 | [ 274 | "arn:aws:execute-api:", 275 | { 276 | "Ref": "AWS::Region" 277 | }, 278 | ":", 279 | { 280 | "Ref": "AWS::AccountId" 281 | }, 282 | ":", 283 | { 284 | "Ref": "ApiGatewayRestApi" 285 | }, 286 | "/*/*" 287 | ] 288 | ] 289 | } 290 | } 291 | } 292 | }, 293 | "Outputs": { 294 | "ServerlessDeploymentBucketName": { 295 | "Value": { 296 | "Ref": "ServerlessDeploymentBucket" 297 | } 298 | }, 299 | "Testfct1LambdaFunctionQualifiedArn": { 300 | "Description": "Current Lambda function version", 301 | "Value": { 302 | "Ref": "Testfct1LambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU" 303 | } 304 | }, 305 | "WarmUpPluginLambdaFunctionQualifiedArn": { 306 | "Description": "Current Lambda function version", 307 | "Value": { 308 | "Ref": "WarmUpPluginLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU" 309 | } 310 | }, 311 | "ServiceEndpoint": { 312 | "Description": "URL of the service endpoint", 313 | "Value": { 314 | "Fn::Join": [ 315 | "", 316 | [ 317 | "https://", 318 | { 319 | "Ref": "ApiGatewayRestApi" 320 | }, 321 | ".execute-api.us-east-1.amazonaws.com/dev" 322 | ] 323 | ] 324 | } 325 | }, 326 | "MasterAliasName": { 327 | "Description": "Master Alias name (serverless-aws-alias plugin)", 328 | "Value": "master", 329 | "Export": { 330 | "Name": "sls-test-project-dev-master" 331 | } 332 | } 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /test/data/sns-stack.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "The AWS CloudFormation template for this Serverless application", 4 | "Resources": { 5 | "ServerlessDeploymentBucket": { 6 | "Type": "AWS::S3::Bucket" 7 | }, 8 | "Testfct1LogGroup": { 9 | "Type": "AWS::Logs::LogGroup", 10 | "Properties": { 11 | "LogGroupName": "/aws/lambda/sls-test-project-dev-testfct1" 12 | } 13 | }, 14 | "IamRoleLambdaExecution": { 15 | "Type": "AWS::IAM::Role", 16 | "Properties": { 17 | "AssumeRolePolicyDocument": { 18 | "Version": "2012-10-17", 19 | "Statement": [ 20 | { 21 | "Effect": "Allow", 22 | "Principal": { 23 | "Service": [ 24 | "lambda.amazonaws.com" 25 | ] 26 | }, 27 | "Action": [ 28 | "sts:AssumeRole" 29 | ] 30 | } 31 | ] 32 | }, 33 | "Policies": [ 34 | { 35 | "PolicyName": { 36 | "Fn::Join": [ 37 | "-", 38 | [ 39 | "dev", 40 | "sls-test-project", 41 | "lambda" 42 | ] 43 | ] 44 | }, 45 | "PolicyDocument": { 46 | "Version": "2012-10-17", 47 | "Statement": [ 48 | { 49 | "Effect": "Allow", 50 | "Action": [ 51 | "logs:CreateLogStream" 52 | ], 53 | "Resource": [ 54 | { 55 | "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/sls-test-project-dev-testfct1:*" 56 | } 57 | ] 58 | }, 59 | { 60 | "Effect": "Allow", 61 | "Action": [ 62 | "logs:PutLogEvents" 63 | ], 64 | "Resource": [ 65 | { 66 | "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/sls-test-project-dev-testfct1:*:*" 67 | } 68 | ] 69 | }, 70 | { 71 | "Effect": "Allow", 72 | "Action": [ 73 | "dynamodb:*" 74 | ], 75 | "Resource": [ 76 | { 77 | "Fn::Join": [ 78 | "/", 79 | [ 80 | { 81 | "Fn::Join": [ 82 | ":", 83 | [ 84 | "arn:aws:dynamodb", 85 | { 86 | "Ref": "AWS::Region" 87 | }, 88 | { 89 | "Ref": "AWS::AccountId" 90 | }, 91 | "table" 92 | ] 93 | ] 94 | }, 95 | { 96 | "Ref": "TestDynamoDbTable" 97 | } 98 | ] 99 | ] 100 | } 101 | ] 102 | }, 103 | { 104 | "Effect": "Allow", 105 | "Action": [ 106 | "dynamodb:GetRecords", 107 | "dynamodb:GetShardIterator", 108 | "dynamodb:DescribeStream", 109 | "dynamodb:ListStreams" 110 | ], 111 | "Resource": [ 112 | { 113 | "Fn::GetAtt": [ 114 | "TestDynamoDbTable", 115 | "StreamArn" 116 | ] 117 | } 118 | ] 119 | } 120 | ] 121 | } 122 | } 123 | ], 124 | "Path": "/", 125 | "RoleName": { 126 | "Fn::Join": [ 127 | "-", 128 | [ 129 | "sls-test-project", 130 | "dev", 131 | "us-east-1", 132 | "lambdaRole" 133 | ] 134 | ] 135 | } 136 | } 137 | }, 138 | "Testfct1LambdaFunction": { 139 | "Type": "AWS::Lambda::Function", 140 | "Properties": { 141 | "Code": { 142 | "S3Bucket": { 143 | "Ref": "ServerlessDeploymentBucket" 144 | }, 145 | "S3Key": "serverless/sls-test-project/dev/1496054947737-2017-05-29T10:49:07.737Z/sls-test-project.zip" 146 | }, 147 | "FunctionName": "sls-test-project-dev-testfct1", 148 | "Handler": "handlers/testfct1/handler.handle", 149 | "MemorySize": 512, 150 | "Role": { 151 | "Fn::GetAtt": [ 152 | "IamRoleLambdaExecution", 153 | "Arn" 154 | ] 155 | }, 156 | "Runtime": "nodejs4.3", 157 | "Timeout": 15, 158 | "Description": "Echo function echoes alias", 159 | "Environment": { 160 | "Variables": { 161 | "SERVERLESS_PROJECT_NAME": "sls-test-project", 162 | "SERVERLESS_PROJECT": "sls-test-project", 163 | "SERVERLESS_STAGE": "dev", 164 | "SERVERLESS_REGION": "us-east-1", 165 | "TEST_TABLE_NAME": { 166 | "Ref": "TestDynamoDbTable" 167 | } 168 | } 169 | } 170 | }, 171 | "DependsOn": [ 172 | "Testfct1LogGroup", 173 | "IamRoleLambdaExecution" 174 | ] 175 | }, 176 | "ApiGatewayRestApi": { 177 | "Type": "AWS::ApiGateway::RestApi", 178 | "Properties": { 179 | "Name": "dev-sls-test-project" 180 | } 181 | }, 182 | "ApiGatewayResourceFunc1": { 183 | "Type": "AWS::ApiGateway::Resource", 184 | "Properties": { 185 | "ParentId": { 186 | "Fn::GetAtt": [ 187 | "ApiGatewayRestApi", 188 | "RootResourceId" 189 | ] 190 | }, 191 | "PathPart": "func1", 192 | "RestApiId": { 193 | "Ref": "ApiGatewayRestApi" 194 | } 195 | } 196 | }, 197 | "ApiGatewayMethodFunc1Get": { 198 | "Type": "AWS::ApiGateway::Method", 199 | "Properties": { 200 | "HttpMethod": "GET", 201 | "RequestParameters": {}, 202 | "ResourceId": { 203 | "Ref": "ApiGatewayResourceFunc1" 204 | }, 205 | "RestApiId": { 206 | "Ref": "ApiGatewayRestApi" 207 | }, 208 | "AuthorizationType": "NONE", 209 | "Integration": { 210 | "IntegrationHttpMethod": "POST", 211 | "Type": "AWS_PROXY", 212 | "Uri": { 213 | "Fn::Join": [ 214 | "", 215 | [ 216 | "arn:aws:apigateway:", 217 | { 218 | "Ref": "AWS::Region" 219 | }, 220 | ":lambda:path/2015-03-31/functions/", 221 | { 222 | "Fn::GetAtt": [ 223 | "Testfct1LambdaFunction", 224 | "Arn" 225 | ] 226 | }, 227 | "/invocations" 228 | ] 229 | ] 230 | } 231 | }, 232 | "MethodResponses": [] 233 | } 234 | }, 235 | "SNSTopicSlstestprojecttopic": { 236 | "Type": "AWS::SNS::Topic", 237 | "Properties": { 238 | "TopicName": "sls-test-project-topic", 239 | "DisplayName": "", 240 | "Subscription": [ 241 | { 242 | "Endpoint": { 243 | "Fn::GetAtt": [ 244 | "Testfct1LambdaFunction", 245 | "Arn" 246 | ] 247 | }, 248 | "Protocol": "lambda" 249 | } 250 | ] 251 | } 252 | }, 253 | "SNSTopicSubscriptionSlstestprojecttopic": { 254 | "Type" : "AWS::SNS::Subscription", 255 | "Properties": { 256 | "Endpoint": { 257 | "Fn::GetAtt": [ 258 | "Testfct1LambdaFunction", 259 | "Arn" 260 | ] 261 | }, 262 | "Protocol": "lambda", 263 | "TopicArn": { 264 | "Fn::GetAtt": [ 265 | "SNSTopicSlstestprojecttopic", 266 | "Arn" 267 | ] 268 | } 269 | } 270 | }, 271 | "Testfct1LambdaPermissionSlstestprojecttopicSNS": { 272 | "Type": "AWS::Lambda::Permission", 273 | "Properties": { 274 | "FunctionName": { 275 | "Fn::GetAtt": [ 276 | "Testfct1LambdaFunction", 277 | "Arn" 278 | ] 279 | }, 280 | "Action": "lambda:InvokeFunction", 281 | "Principal": "sns.amazonaws.com", 282 | "SourceArn": { 283 | "Fn::Join": [ 284 | "", 285 | [ 286 | "arn:aws:sns:", 287 | { 288 | "Ref": "AWS::Region" 289 | }, 290 | ":", 291 | { 292 | "Ref": "AWS::AccountId" 293 | }, 294 | ":", 295 | "sls-test-project-topic" 296 | ] 297 | ] 298 | } 299 | } 300 | }, 301 | "Testfct1EventSourceMappingDynamodbTestDynamoDbTable": { 302 | "Type": "AWS::Lambda::EventSourceMapping", 303 | "DependsOn": "IamRoleLambdaExecution", 304 | "Properties": { 305 | "BatchSize": 10, 306 | "EventSourceArn": { 307 | "Fn::GetAtt": [ 308 | "TestDynamoDbTable", 309 | "StreamArn" 310 | ] 311 | }, 312 | "FunctionName": { 313 | "Fn::GetAtt": [ 314 | "Testfct1LambdaFunction", 315 | "Arn" 316 | ] 317 | }, 318 | "StartingPosition": "TRIM_HORIZON", 319 | "Enabled": "True" 320 | } 321 | }, 322 | "TestDynamoDbTable": { 323 | "Type": "AWS::DynamoDB::Table", 324 | "DeletionPolicy": "Delete", 325 | "Properties": { 326 | "AttributeDefinitions": [ 327 | { 328 | "AttributeName": "myKey", 329 | "AttributeType": "S" 330 | } 331 | ], 332 | "KeySchema": [ 333 | { 334 | "AttributeName": "myKey", 335 | "KeyType": "HASH" 336 | } 337 | ], 338 | "ProvisionedThroughput": { 339 | "ReadCapacityUnits": 1, 340 | "WriteCapacityUnits": 1 341 | }, 342 | "StreamSpecification": { 343 | "StreamViewType": "NEW_AND_OLD_IMAGES" 344 | } 345 | } 346 | } 347 | }, 348 | "Outputs": { 349 | "ServerlessDeploymentBucketName": { 350 | "Value": { 351 | "Ref": "ServerlessDeploymentBucket" 352 | } 353 | }, 354 | "ServiceEndpoint": { 355 | "Description": "URL of the service endpoint", 356 | "Value": { 357 | "Fn::Join": [ 358 | "", 359 | [ 360 | "https://", 361 | { 362 | "Ref": "ApiGatewayRestApi" 363 | }, 364 | ".execute-api.us-east-1.amazonaws.com/dev" 365 | ] 366 | ] 367 | } 368 | }, 369 | "TestDynamoDbTableName": { 370 | "Description": "Test DynamoDB Table Name", 371 | "Value": { 372 | "Ref": "TestDynamoDbTable" 373 | } 374 | } 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Unit tests for plugin class. 4 | */ 5 | 6 | const BbPromise = require('bluebird'); 7 | const { getInstalledPathSync } = require('get-installed-path'); 8 | const chai = require('chai'); 9 | const sinon = require('sinon'); 10 | const AwsAlias = require('../index'); 11 | 12 | const serverlessPath = getInstalledPathSync('serverless', { local: true }); 13 | const AwsProvider = require(`${serverlessPath}/lib/plugins/aws/provider/awsProvider`); 14 | const Serverless = require(`${serverlessPath}/lib/Serverless`); 15 | 16 | chai.use(require('chai-as-promised')); 17 | chai.use(require('sinon-chai')); 18 | const expect = chai.expect; 19 | 20 | describe('AwsAlias', () => { 21 | let serverless; 22 | let options; 23 | let sandbox; 24 | 25 | before(() => { 26 | sandbox = sinon.createSandbox(); 27 | }); 28 | 29 | beforeEach(() => { 30 | options = { 31 | stage: 'myStage', 32 | region: 'us-east-1', 33 | }; 34 | serverless = new Serverless(options); 35 | serverless.cli = new serverless.classes.CLI(serverless); 36 | serverless.service.service = 'myService'; 37 | serverless.setProvider('aws', new AwsProvider(serverless, options)); 38 | }); 39 | 40 | afterEach(() => { 41 | sandbox.restore(); 42 | }); 43 | 44 | describe('constructor', () => { 45 | it('should initialize the plugin without options', () => { 46 | const awsAlias = new AwsAlias(serverless); 47 | 48 | expect(awsAlias).to.have.property('_serverless', serverless); 49 | expect(awsAlias).to.have.property('_options').to.deep.equal({}); 50 | }); 51 | 52 | it('should initialize the plugin with empty options', () => { 53 | const awsAlias = new AwsAlias(serverless, {}); 54 | 55 | expect(awsAlias).to.have.property('_serverless', serverless); 56 | expect(awsAlias).to.have.property('_options').to.deep.equal({}); 57 | }); 58 | 59 | it('should initialize the plugin with options', () => { 60 | const awsAlias = new AwsAlias(serverless, options); 61 | 62 | expect(awsAlias).to.have.property('_serverless', serverless); 63 | expect(awsAlias).to.have.property('_options').to.deep.equal(options); 64 | }); 65 | 66 | it('should add the logs api command', () => { 67 | const command = { 68 | options: {}, 69 | commands: {}, 70 | }; 71 | const getCommandStub = sandbox.stub(serverless.pluginManager, 'getCommand'); 72 | getCommandStub.returns(command); 73 | const awsAlias = new AwsAlias(serverless); 74 | expect(awsAlias).to.be.an('object'); 75 | expect(command).to.have.nested.property('commands.api'); 76 | }); 77 | }); 78 | 79 | it('should expose standard properties', () => { 80 | const awsAlias = new AwsAlias(serverless, options); 81 | 82 | awsAlias._stackName = 'myStack'; 83 | 84 | expect(awsAlias).to.have.property('serverless', serverless); 85 | expect(awsAlias).to.have.property('options').to.deep.equal(options); 86 | expect(awsAlias).to.have.property('commands', awsAlias._commands); 87 | expect(awsAlias).to.have.property('hooks', awsAlias._hooks); 88 | expect(awsAlias).to.have.property('provider', awsAlias._provider); 89 | expect(awsAlias).to.have.property('stackName', 'myStack'); 90 | }); 91 | 92 | describe('hook', () => { 93 | let sandbox; 94 | let awsAlias; 95 | let validateStub; 96 | let configureAliasStackStub; 97 | let createAliasStackStub; 98 | let aliasStackLoadCurrentCFStackAndDependenciesStub; 99 | let aliasRestructureStackStub; 100 | let setBucketNameStub; 101 | let uploadAliasArtifactsStub; 102 | let updateAliasStackStub; 103 | let collectUserResourcesStub; 104 | let logsValidateStub; 105 | let logsGetLogStreamsStub; 106 | let logsShowLogsStub; 107 | let removeAliasStub; 108 | let listAliasesStub; 109 | let apiLogsValidateStub; 110 | let apiLogsGetLogStreamsStub; 111 | let apiLogsShowLogsStub; 112 | 113 | before(() => { 114 | sandbox = sinon.createSandbox(); 115 | awsAlias = new AwsAlias(serverless, options); 116 | }); 117 | 118 | beforeEach(() => { 119 | validateStub = sandbox.stub(awsAlias, 'validate'); 120 | configureAliasStackStub = sandbox.stub(awsAlias, 'configureAliasStack'); 121 | createAliasStackStub = sandbox.stub(awsAlias, 'createAliasStack'); 122 | aliasStackLoadCurrentCFStackAndDependenciesStub = sandbox.stub(awsAlias, 'aliasStackLoadCurrentCFStackAndDependencies'); 123 | aliasRestructureStackStub = sandbox.stub(awsAlias, 'aliasRestructureStack'); 124 | setBucketNameStub = sandbox.stub(awsAlias, 'setBucketName'); 125 | uploadAliasArtifactsStub = sandbox.stub(awsAlias, 'uploadAliasArtifacts'); 126 | updateAliasStackStub = sandbox.stub(awsAlias, 'updateAliasStack'); 127 | collectUserResourcesStub = sandbox.stub(awsAlias, 'collectUserResources'); 128 | logsValidateStub = sandbox.stub(awsAlias, 'logsValidate'); 129 | logsGetLogStreamsStub = sandbox.stub(awsAlias, 'logsGetLogStreams'); 130 | logsShowLogsStub = sandbox.stub(awsAlias, 'logsShowLogs'); 131 | removeAliasStub = sandbox.stub(awsAlias, 'removeAlias'); 132 | listAliasesStub = sandbox.stub(awsAlias, 'listAliases'); 133 | apiLogsValidateStub = sandbox.stub(awsAlias, 'apiLogsValidate'); 134 | apiLogsGetLogStreamsStub = sandbox.stub(awsAlias, 'apiLogsGetLogStreams'); 135 | apiLogsShowLogsStub = sandbox.stub(awsAlias, 'apiLogsShowLogs'); 136 | }); 137 | 138 | afterEach(() => { 139 | sandbox.restore(); 140 | }); 141 | 142 | it('before:package:initialize should resolve', () => { 143 | validateStub.returns(BbPromise.resolve()); 144 | return expect(awsAlias.hooks['before:package:initialize']()).to.eventually.be.fulfilled 145 | .then(() => expect(validateStub).to.be.calledOnce); 146 | }); 147 | 148 | it('before:aws:package:finalize:mergeCustomProviderResources should resolve', () => { 149 | validateStub.returns(BbPromise.resolve()); 150 | return expect(awsAlias.hooks['before:aws:package:finalize:mergeCustomProviderResources']()).to.eventually.be.fulfilled 151 | .then(() => expect(collectUserResourcesStub).to.be.calledOnce); 152 | }); 153 | 154 | it('before:deploy:deploy should resolve', () => { 155 | configureAliasStackStub.returns(BbPromise.resolve()); 156 | return expect(awsAlias.hooks['before:deploy:deploy']()).to.eventually.be.fulfilled 157 | .then(() => BbPromise.all([ 158 | expect(validateStub).to.be.calledOnce, 159 | expect(configureAliasStackStub).to.be.calledOnce, 160 | ])); 161 | }); 162 | 163 | it('before:aws:deploy:deploy:createStack should resolve', () => { 164 | aliasStackLoadCurrentCFStackAndDependenciesStub.returns(BbPromise.resolve([])); 165 | aliasRestructureStackStub.returns(BbPromise.resolve()); 166 | return expect(awsAlias.hooks['before:aws:deploy:deploy:createStack']()).to.eventually.be.fulfilled 167 | .then(() => BbPromise.join( 168 | expect(aliasStackLoadCurrentCFStackAndDependenciesStub).to.be.calledOnce, 169 | expect(aliasRestructureStackStub).to.be.calledOnce 170 | )); 171 | }); 172 | 173 | it('after:aws:deploy:deploy:createStack should resolve', () => { 174 | createAliasStackStub.returns(BbPromise.resolve()); 175 | return expect(awsAlias.hooks['after:aws:deploy:deploy:createStack']()).to.eventually.be.fulfilled 176 | .then(() => expect(createAliasStackStub).to.be.calledOnce); 177 | }); 178 | 179 | it('after:aws:deploy:deploy:uploadArtifacts should resolve', () => { 180 | return expect(awsAlias.hooks['after:aws:deploy:deploy:uploadArtifacts']()).to.eventually.be.fulfilled; 181 | }); 182 | 183 | it('after:aws:deploy:deploy:updateStack should resolve', () => { 184 | setBucketNameStub.returns(BbPromise.resolve()); 185 | uploadAliasArtifactsStub.returns(BbPromise.resolve()); 186 | updateAliasStackStub.returns(BbPromise.resolve()); 187 | return expect(awsAlias.hooks['after:aws:deploy:deploy:updateStack']()).to.eventually.be.fulfilled 188 | .then(() => { 189 | expect(setBucketNameStub).to.be.calledOnce; 190 | expect(uploadAliasArtifactsStub).to.be.calledOnce; 191 | expect(updateAliasStackStub).to.be.calledOnce; 192 | return null; 193 | }); 194 | }); 195 | 196 | it('after:info:info should resolve', () => { 197 | validateStub.returns(BbPromise.resolve()); 198 | listAliasesStub.returns(BbPromise.resolve()); 199 | return expect(awsAlias.hooks['after:info:info']()).to.eventually.be.fulfilled 200 | .then(() => BbPromise.join( 201 | expect(validateStub).to.be.calledOnce, 202 | expect(listAliasesStub).to.be.calledOnce 203 | )); 204 | }); 205 | 206 | it('logs:logs should resolve', () => { 207 | logsValidateStub.returns(BbPromise.resolve()); 208 | logsGetLogStreamsStub.returns(BbPromise.resolve()); 209 | logsShowLogsStub.returns(BbPromise.resolve()); 210 | return expect(awsAlias.hooks['logs:logs']()).to.eventually.be.fulfilled 211 | .then(() => BbPromise.join( 212 | expect(logsValidateStub).to.be.calledOnce, 213 | expect(logsGetLogStreamsStub).to.be.calledOnce, 214 | expect(logsShowLogsStub).to.be.calledOnce 215 | )); 216 | }); 217 | 218 | it('logs:api:logs should resolve', () => { 219 | apiLogsValidateStub.returns(BbPromise.resolve()); 220 | apiLogsGetLogStreamsStub.returns(BbPromise.resolve()); 221 | apiLogsShowLogsStub.returns(BbPromise.resolve()); 222 | return expect(awsAlias.hooks['logs:api:logs']()).to.eventually.be.fulfilled 223 | .then(() => BbPromise.join( 224 | expect(apiLogsValidateStub).to.be.calledOnce, 225 | expect(apiLogsGetLogStreamsStub).to.be.calledOnce, 226 | expect(apiLogsShowLogsStub).to.be.calledOnce 227 | )); 228 | }); 229 | 230 | describe('before:remove:remove', () => { 231 | it('should resolve', () => { 232 | awsAlias._validated = true; 233 | return expect(awsAlias.hooks['before:remove:remove']()).to.eventually.be.fulfilled; 234 | }); 235 | 236 | it('should reject if alias validation did not run', () => { 237 | awsAlias._validated = false; 238 | return expect(awsAlias.hooks['before:remove:remove']()).to.be.rejectedWith(/Use "serverless alias remove/); 239 | }); 240 | }); 241 | 242 | it('alias:remove:remove should resolve', () => { 243 | validateStub.returns(BbPromise.resolve()); 244 | aliasStackLoadCurrentCFStackAndDependenciesStub.returns(BbPromise.resolve([])); 245 | removeAliasStub.returns(BbPromise.resolve()); 246 | return expect(awsAlias.hooks['alias:remove:remove']()).to.eventually.be.fulfilled 247 | .then(() => BbPromise.join( 248 | expect(validateStub).to.be.calledOnce, 249 | expect(aliasStackLoadCurrentCFStackAndDependenciesStub).to.be.calledOnce, 250 | expect(removeAliasStub).to.be.calledOnce 251 | )); 252 | }); 253 | }); 254 | }); 255 | -------------------------------------------------------------------------------- /test/removeAlias.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Unit tests for createAliasStack.. 4 | */ 5 | 6 | const { getInstalledPathSync } = require('get-installed-path'); 7 | const BbPromise = require('bluebird'); 8 | const chai = require('chai'); 9 | const sinon = require('sinon'); 10 | const _ = require('lodash'); 11 | const AWSAlias = require('../index'); 12 | 13 | const serverlessPath = getInstalledPathSync('serverless', { local: true }); 14 | const AwsProvider = require(`${serverlessPath}/lib/plugins/aws/provider/awsProvider`); 15 | const Serverless = require(`${serverlessPath}/lib/Serverless`); 16 | 17 | chai.use(require('chai-as-promised')); 18 | chai.use(require('sinon-chai')); 19 | chai.use(require('chai-subset')); 20 | const expect = chai.expect; 21 | 22 | describe('removeAlias', () => { 23 | let serverless; 24 | let options; 25 | let awsAlias; 26 | // Sinon and stubs for SLS CF access 27 | let sandbox; 28 | let providerRequestStub; 29 | let monitorStackStub; 30 | let logStub; 31 | let slsStack1; 32 | let aliasStack1; 33 | let aliasStack2; 34 | 35 | before(() => { 36 | sandbox = sinon.createSandbox(); 37 | }); 38 | 39 | beforeEach(() => { 40 | serverless = new Serverless(); 41 | options = { 42 | alias: 'myAlias', 43 | stage: 'myStage', 44 | region: 'us-east-1', 45 | }; 46 | serverless.setProvider('aws', new AwsProvider(serverless, options)); 47 | serverless.cli = new serverless.classes.CLI(serverless); 48 | serverless.service.service = 'testService'; 49 | serverless.service.provider.compiledCloudFormationAliasTemplate = {}; 50 | awsAlias = new AWSAlias(serverless, options); 51 | providerRequestStub = sandbox.stub(awsAlias._provider, 'request'); 52 | monitorStackStub = sandbox.stub(awsAlias, 'monitorStack'); 53 | logStub = sandbox.stub(serverless.cli, 'log'); 54 | 55 | slsStack1 = _.cloneDeep(require('./data/sls-stack-1.json')); 56 | aliasStack1 = _.cloneDeep(require('./data/alias-stack-1.json')); 57 | aliasStack2 = _.cloneDeep(require('./data/alias-stack-2.json')); 58 | 59 | logStub.returns(); 60 | }); 61 | 62 | afterEach(() => { 63 | sandbox.restore(); 64 | }); 65 | 66 | describe('#removeAlias()', () => { 67 | let aliasCreateStackChangesStub; 68 | let aliasRemoveAliasStackStub; 69 | let aliasApplyStackChangesStub; 70 | let pluginManagerSpawnStub; 71 | 72 | beforeEach(() => { 73 | aliasApplyStackChangesStub = sandbox.stub(awsAlias, 'aliasApplyStackChanges'); 74 | aliasCreateStackChangesStub = sandbox.stub(awsAlias, 'aliasCreateStackChanges'); 75 | aliasRemoveAliasStackStub = sandbox.stub(awsAlias, 'aliasRemoveAliasStack'); 76 | pluginManagerSpawnStub = sandbox.stub(awsAlias._serverless.pluginManager, 'spawn'); 77 | }); 78 | 79 | it('should do nothing with noDeploy', () => { 80 | awsAlias._options = { noDeploy: true }; 81 | 82 | return expect(awsAlias.removeAlias()).to.be.fulfilled 83 | .then(() => BbPromise.all([ 84 | expect(aliasApplyStackChangesStub).to.not.have.been.called, 85 | expect(aliasCreateStackChangesStub).to.not.have.been.called, 86 | expect(aliasRemoveAliasStackStub).to.not.have.been.called, 87 | expect(pluginManagerSpawnStub).to.not.have.been.called, 88 | ])); 89 | }); 90 | 91 | it('should error if an alias is deployed on stage removal', () => { 92 | awsAlias._options = { alias: 'myStage' }; 93 | awsAlias._alias = 'master'; 94 | slsStack1.Outputs.MasterAliasName = { 95 | Value: 'master' 96 | }; 97 | 98 | expect(() => awsAlias.removeAlias(slsStack1, [ aliasStack1 ], aliasStack2)).to.throw(/myAlias/); 99 | return BbPromise.all([ 100 | expect(aliasApplyStackChangesStub).to.not.have.been.called, 101 | expect(aliasCreateStackChangesStub).to.not.have.been.called, 102 | expect(aliasRemoveAliasStackStub).to.not.have.been.called, 103 | expect(pluginManagerSpawnStub).to.not.have.been.called, 104 | ]); 105 | }); 106 | 107 | it('should error if the master alias is not deployed on stage removal', () => { 108 | awsAlias._options = { alias: 'myStage' }; 109 | awsAlias._alias = 'master'; 110 | slsStack1.Outputs.MasterAliasName = { 111 | Value: 'master' 112 | }; 113 | 114 | expect(() => awsAlias.removeAlias(slsStack1, [], {})).to.throw(/Internal error/); 115 | return BbPromise.all([ 116 | expect(aliasApplyStackChangesStub).to.not.have.been.called, 117 | expect(aliasCreateStackChangesStub).to.not.have.been.called, 118 | expect(aliasRemoveAliasStackStub).to.not.have.been.called, 119 | expect(pluginManagerSpawnStub).to.not.have.been.called, 120 | ]); 121 | }); 122 | 123 | it('should remove alias and service stack on stage removal', () => { 124 | awsAlias._options = { alias: 'myStage' }; 125 | awsAlias._alias = 'master'; 126 | slsStack1.Outputs.MasterAliasName = { 127 | Value: 'master' 128 | }; 129 | 130 | return expect(awsAlias.removeAlias(slsStack1, [], aliasStack2)).to.be.fulfilled 131 | .then(() => BbPromise.all([ 132 | expect(aliasApplyStackChangesStub).to.not.have.been.called, 133 | expect(aliasCreateStackChangesStub).to.not.have.been.called, 134 | expect(aliasRemoveAliasStackStub).to.have.been.calledOnce, 135 | expect(pluginManagerSpawnStub).to.have.been.calledWithExactly('remove'), 136 | ])); 137 | }); 138 | 139 | it('should remove alias stack', () => { 140 | slsStack1.Outputs.MasterAliasName = { 141 | Value: 'master' 142 | }; 143 | aliasApplyStackChangesStub.returns([ slsStack1, [ aliasStack2 ], aliasStack1 ]); 144 | aliasCreateStackChangesStub.returns([ slsStack1, [ aliasStack2 ], aliasStack1 ]); 145 | aliasRemoveAliasStackStub.returns([ slsStack1, [ aliasStack2 ], aliasStack1 ]); 146 | 147 | return expect(awsAlias.removeAlias(slsStack1, [ aliasStack2 ], aliasStack1)).to.be.fulfilled 148 | .then(() => BbPromise.all([ 149 | expect(aliasCreateStackChangesStub).to.have.been.calledOnce, 150 | expect(aliasRemoveAliasStackStub).to.have.been.calledOnce, 151 | expect(aliasApplyStackChangesStub).to.have.been.calledOnce, 152 | expect(pluginManagerSpawnStub).to.not.have.been.called, 153 | ])); 154 | }); 155 | }); 156 | 157 | describe('#aliasRemoveAliasStack()', () => { 158 | it('should call CF to remove stack', () => { 159 | const requestResult = { 160 | status: 'ok' 161 | }; 162 | providerRequestStub.returns(BbPromise.resolve(requestResult)); 163 | monitorStackStub.returns(BbPromise.resolve()); 164 | 165 | return expect(awsAlias.aliasRemoveAliasStack(slsStack1, [ aliasStack2 ], aliasStack1)).to.be.fulfilled 166 | .then(() => BbPromise.all([ 167 | expect(providerRequestStub).to.have.been.calledOnce, 168 | 169 | ])); 170 | }); 171 | 172 | it('should throw an error if the stack does not exist', () => { 173 | providerRequestStub.returns(BbPromise.reject(new Error('stack does not exist'))); 174 | monitorStackStub.returns(BbPromise.resolve()); 175 | 176 | return expect(awsAlias.aliasRemoveAliasStack(slsStack1, [ aliasStack2 ], aliasStack1)).to.be.rejectedWith('is not deployed') 177 | .then(() => BbPromise.all([ 178 | expect(providerRequestStub).to.have.been.calledOnce, 179 | ])); 180 | }); 181 | 182 | it('should propagate CF errors', () => { 183 | providerRequestStub.returns(BbPromise.reject(new Error('CF Error'))); 184 | monitorStackStub.returns(BbPromise.resolve()); 185 | 186 | return expect(awsAlias.aliasRemoveAliasStack(slsStack1, [ aliasStack2 ], aliasStack1)).to.be.rejectedWith('CF Error') 187 | .then(() => BbPromise.all([ 188 | expect(providerRequestStub).to.have.been.calledOnce, 189 | ])); 190 | }); 191 | }); 192 | 193 | describe('#aliasApplyStackChanges()', () => { 194 | it('should call CF and update stage stack', () => { 195 | const requestResult = { 196 | status: 'ok' 197 | }; 198 | providerRequestStub.returns(BbPromise.resolve(requestResult)); 199 | monitorStackStub.returns(BbPromise.resolve()); 200 | 201 | return expect(awsAlias.aliasApplyStackChanges(slsStack1, [ aliasStack2 ], aliasStack1)).to.be.fulfilled 202 | .then(() => BbPromise.all([ 203 | expect(providerRequestStub).to.have.been.calledOnce, 204 | expect(providerRequestStub.getCall(0).args[0]).to.equal('CloudFormation'), 205 | expect(providerRequestStub.getCall(0).args[1]).to.equal('updateStack'), 206 | expect(providerRequestStub.getCall(0).args[2]).to.containSubset({ StackName: 'testService-myStage' }), 207 | ])); 208 | }); 209 | 210 | it('should resolve if no updates are applied', () => { 211 | providerRequestStub.rejects(new Error('No updates are to be performed.')); 212 | monitorStackStub.returns(BbPromise.resolve()); 213 | 214 | return expect(awsAlias.aliasApplyStackChanges(slsStack1, [ aliasStack2 ], aliasStack1)).to.be.fulfilled 215 | .then(() => BbPromise.all([ 216 | expect(providerRequestStub).to.have.been.calledOnce, 217 | ])); 218 | }); 219 | 220 | it('should propagate CF errors', () => { 221 | providerRequestStub.returns(BbPromise.reject(new Error('CF Error'))); 222 | monitorStackStub.returns(BbPromise.resolve()); 223 | 224 | return expect(awsAlias.aliasApplyStackChanges(slsStack1, [ aliasStack2 ], aliasStack1)).to.be.rejectedWith('CF Error') 225 | .then(() => BbPromise.all([ 226 | expect(providerRequestStub).to.have.been.calledOnce, 227 | ])); 228 | }); 229 | }); 230 | 231 | it('should merge custom tags', () => { 232 | const requestResult = { 233 | status: 'ok' 234 | }; 235 | providerRequestStub.returns(BbPromise.resolve(requestResult)); 236 | monitorStackStub.returns(BbPromise.resolve()); 237 | awsAlias._serverless.service.provider.stackTags = { tag1: '1', tag2: '2' }; 238 | 239 | return expect(awsAlias.aliasApplyStackChanges(slsStack1, [ aliasStack2 ], aliasStack1)).to.be.fulfilled 240 | .then(() => BbPromise.all([ 241 | expect(providerRequestStub).to.have.been.calledOnce, 242 | expect(providerRequestStub.getCall(0).args[2]).to.containSubset({ Tags: [ 243 | { 244 | Key: 'tag1', 245 | Value: '1' 246 | }, 247 | { 248 | Key: 'tag2', 249 | Value: '2' 250 | } 251 | ] }), 252 | ])); 253 | }); 254 | 255 | it('should use custom stack policy', () => { 256 | const requestResult = { 257 | status: 'ok' 258 | }; 259 | providerRequestStub.returns(BbPromise.resolve(requestResult)); 260 | monitorStackStub.returns(BbPromise.resolve()); 261 | awsAlias._serverless.service.provider.stackPolicy = [{ title: 'myPolicy' }]; 262 | 263 | return expect(awsAlias.aliasApplyStackChanges(slsStack1, [ aliasStack2 ], aliasStack1)).to.be.fulfilled 264 | .then(() => BbPromise.all([ 265 | expect(providerRequestStub).to.have.been.calledOnce, 266 | expect(providerRequestStub.getCall(0).args[2]).to.containSubset({ StackPolicyBody: '{"Statement":[{"title":"myPolicy"}]}' }), 267 | ])); 268 | }); 269 | }); 270 | -------------------------------------------------------------------------------- /test/stackops/init.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Unit tests for initialization. 4 | */ 5 | 6 | const { getInstalledPathSync } = require('get-installed-path'); 7 | const _ = require('lodash'); 8 | const BbPromise = require('bluebird'); 9 | const chai = require('chai'); 10 | const sinon = require('sinon'); 11 | const AWSAlias = require('../../index'); 12 | 13 | const serverlessPath = getInstalledPathSync('serverless', { local: true }); 14 | const AwsProvider = require(`${serverlessPath}/lib/plugins/aws/provider/awsProvider`); 15 | const Serverless = require(`${serverlessPath}/lib/Serverless`); 16 | 17 | chai.use(require('chai-as-promised')); 18 | chai.use(require('sinon-chai')); 19 | const expect = chai.expect; 20 | 21 | describe('SNS Events', () => { 22 | let serverless; 23 | let options; 24 | let awsAlias; 25 | // Sinon and stubs for SLS CF access 26 | let sandbox; 27 | let logStub; 28 | 29 | before(() => { 30 | sandbox = sinon.createSandbox(); 31 | }); 32 | 33 | beforeEach(() => { 34 | options = { 35 | alias: 'myAlias', 36 | stage: 'myStage', 37 | region: 'us-east-1', 38 | }; 39 | serverless = new Serverless(options); 40 | serverless.setProvider('aws', new AwsProvider(serverless)); 41 | serverless.cli = new serverless.classes.CLI(serverless); 42 | serverless.service.service = 'testService'; 43 | serverless.service.provider.compiledCloudFormationAliasTemplate = {}; 44 | awsAlias = new AWSAlias(serverless, options); 45 | 46 | // Disable logging 47 | logStub = sandbox.stub(serverless.cli, 'log'); 48 | logStub.returns(); 49 | }); 50 | 51 | afterEach(() => { 52 | sandbox.restore(); 53 | }); 54 | 55 | describe('#aliasInit()', () => { 56 | it('should set alias flags', () => { 57 | serverless.service.provider.compiledCloudFormationTemplate = _.cloneDeep(require('../data/sls-stack-1.json')); 58 | const aliasStack = serverless.service.provider.compiledCloudFormationAliasTemplate = _.cloneDeep(require('../data/alias-stack-1.json')); 59 | return expect(awsAlias.aliasInit({}, [], {})).to.be.fulfilled 60 | .then(() => BbPromise.all([ 61 | expect(aliasStack).to.have.property('Outputs') 62 | .that.has.property('AliasFlags') 63 | .that.deep.equals({ 64 | Description: 'Alias flags.', 65 | Value: { hasRole: false } 66 | }) 67 | ])); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/stackops/lambdaRole.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Unit tests for lambda role transformations. 4 | */ 5 | 6 | const { getInstalledPathSync } = require('get-installed-path'); 7 | const _ = require('lodash'); 8 | const chai = require('chai'); 9 | const sinon = require('sinon'); 10 | const AWSAlias = require('../../index'); 11 | 12 | const serverlessPath = getInstalledPathSync('serverless', { local: true }); 13 | const AwsProvider = require(`${serverlessPath}/lib/plugins/aws/provider/awsProvider`); 14 | const Serverless = require(`${serverlessPath}/lib/Serverless`); 15 | 16 | chai.use(require('chai-as-promised')); 17 | chai.use(require('sinon-chai')); 18 | const expect = chai.expect; 19 | 20 | describe('lambdaRole', () => { 21 | let serverless; 22 | let options; 23 | let awsAlias; 24 | // Sinon and stubs for SLS CF access 25 | let sandbox; 26 | let logStub; 27 | 28 | before(() => { 29 | sandbox = sinon.createSandbox(); 30 | }); 31 | 32 | beforeEach(() => { 33 | options = { 34 | alias: 'myAlias', 35 | stage: 'myStage', 36 | region: 'us-east-1', 37 | }; 38 | serverless = new Serverless(); 39 | serverless.setProvider('aws', new AwsProvider(serverless, options)); 40 | serverless.cli = new serverless.classes.CLI(serverless); 41 | serverless.service.service = 'testService'; 42 | serverless.service.provider.compiledCloudFormationAliasTemplate = {}; 43 | awsAlias = new AWSAlias(serverless, options); 44 | 45 | // Disable logging 46 | logStub = sandbox.stub(serverless.cli, 'log'); 47 | logStub.returns(); 48 | 49 | return awsAlias.validate(); 50 | }); 51 | 52 | afterEach(() => { 53 | sandbox.restore(); 54 | }); 55 | 56 | describe('#aliasHandleLambdaRole()', () => { 57 | let stack; 58 | 59 | beforeEach(() => { 60 | stack = _.cloneDeep(require('../data/sls-stack-1.json')); 61 | }); 62 | 63 | it('should succeed with standard template', () => { 64 | serverless.service.provider.compiledCloudFormationTemplate = stack; 65 | return expect(awsAlias.aliasHandleLambdaRole({}, [], {})).to.be.fulfilled; 66 | }); 67 | 68 | it('should remove old global IAM role when there are no references', () => { 69 | const currentTemplate = { 70 | Resources: { 71 | IamRoleLambdaExecution: {} 72 | }, 73 | Outputs: {} 74 | }; 75 | serverless.service.provider.compiledCloudFormationTemplate = stack; 76 | return expect(awsAlias.aliasHandleLambdaRole(currentTemplate, [], {})).to.be.fulfilled 77 | .then(() => expect(currentTemplate).to.not.have.a.property('IamRoleLambdaExecution')); 78 | }); 79 | 80 | it('should retain existing alias roles', () => { 81 | const aliasTemplates = [{ 82 | Resources: {}, 83 | Outputs: { 84 | ServerlessAliasName: { 85 | Description: 'The current alias', 86 | Value: 'testAlias' 87 | } 88 | } 89 | }]; 90 | const currentTemplate = { 91 | Resources: { 92 | IamRoleLambdaExecution: {}, 93 | IamRoleLambdaExecutiontestAlias: {} 94 | }, 95 | Outputs: {} 96 | }; 97 | const stackTemplate = serverless.service.provider.compiledCloudFormationTemplate = stack; 98 | return expect(awsAlias.aliasHandleLambdaRole(currentTemplate, aliasTemplates, {})).to.be.fulfilled 99 | .then(() => expect(stackTemplate).to.have.a.nested.property('Resources.IamRoleLambdaExecutiontestAlias')); 100 | }); 101 | 102 | it('should retain custom stack roles', () => { 103 | const aliasTemplates = [{ 104 | Resources: {}, 105 | Outputs: { 106 | ServerlessAliasName: { 107 | Description: 'The current alias', 108 | Value: 'testAlias' 109 | } 110 | } 111 | }]; 112 | const currentTemplate = { 113 | Resources: { 114 | IamRoleLambdaExecution: {}, 115 | IamRoleLambdaExecutiontestAlias: {} 116 | }, 117 | Outputs: {} 118 | }; 119 | 120 | const customRoleStack = _.cloneDeep(require('../data/sls-stack-3.json')); 121 | const stackTemplate = serverless.service.provider.compiledCloudFormationTemplate = customRoleStack; 122 | return expect(awsAlias.aliasHandleLambdaRole(currentTemplate, aliasTemplates, {})).to.be.fulfilled 123 | .then(() => expect(stackTemplate).to.have.a.nested.property('Resources.IamRoleLambdaExecutiontestAlias')); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/stackops/snsEvents.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Unit tests for SNS events. 4 | */ 5 | 6 | const { getInstalledPathSync } = require('get-installed-path'); 7 | const _ = require('lodash'); 8 | const BbPromise = require('bluebird'); 9 | const chai = require('chai'); 10 | const sinon = require('sinon'); 11 | const AWSAlias = require('../../index'); 12 | 13 | const serverlessPath = getInstalledPathSync('serverless', { local: true }); 14 | const AwsProvider = require(`${serverlessPath}/lib/plugins/aws/provider/awsProvider`); 15 | const Serverless = require(`${serverlessPath}/lib/Serverless`); 16 | 17 | chai.use(require('chai-as-promised')); 18 | chai.use(require('sinon-chai')); 19 | const expect = chai.expect; 20 | 21 | describe('SNS Events', () => { 22 | let serverless; 23 | let options; 24 | let awsAlias; 25 | // Sinon and stubs for SLS CF access 26 | let sandbox; 27 | let logStub; 28 | 29 | before(() => { 30 | sandbox = sinon.createSandbox(); 31 | }); 32 | 33 | beforeEach(() => { 34 | options = { 35 | alias: 'myAlias', 36 | stage: 'myStage', 37 | region: 'us-east-1', 38 | }; 39 | serverless = new Serverless(options); 40 | serverless.setProvider('aws', new AwsProvider(serverless)); 41 | serverless.cli = new serverless.classes.CLI(serverless); 42 | serverless.service.service = 'testService'; 43 | serverless.service.provider.compiledCloudFormationAliasTemplate = {}; 44 | awsAlias = new AWSAlias(serverless, options); 45 | 46 | // Disable logging 47 | logStub = sandbox.stub(serverless.cli, 'log'); 48 | logStub.returns(); 49 | }); 50 | 51 | afterEach(() => { 52 | sandbox.restore(); 53 | }); 54 | 55 | describe('#aliasHandleSNSEvents()', () => { 56 | let stack1; 57 | let aliasStack1; 58 | let snsStack1; 59 | 60 | beforeEach(() => { 61 | stack1 = _.cloneDeep(require('../data/sls-stack-1.json')); 62 | aliasStack1 = _.cloneDeep(require('../data/alias-stack-1.json')); 63 | snsStack1 = _.cloneDeep(require('../data/sns-stack.json')); 64 | }); 65 | 66 | it('should succeed with standard template', () => { 67 | serverless.service.provider.compiledCloudFormationTemplate = stack1; 68 | serverless.service.provider.compiledCloudFormationAliasTemplate = aliasStack1; 69 | return expect(awsAlias.aliasHandleSNSEvents({}, [], {})).to.be.fulfilled; 70 | }); 71 | 72 | it('should move resources to alias stack', () => { 73 | const snsStack = serverless.service.provider.compiledCloudFormationTemplate = snsStack1; 74 | const aliasStack = serverless.service.provider.compiledCloudFormationAliasTemplate = aliasStack1; 75 | return expect(awsAlias.aliasHandleSNSEvents({}, [], {})).to.be.fulfilled 76 | .then(() => BbPromise.all([ 77 | expect(snsStack).to.not.have.a.nested.property('Resources.SNSTopicSlstestprojecttopic'), 78 | expect(snsStack).to.not.have.a.nested.property('Resources.SNSTopicSubscriptionSlstestprojecttopic'), 79 | expect(snsStack).to.not.have.a.nested.property('Resources.Testfct1LambdaPermissionSlstestprojecttopicSNS'), 80 | expect(aliasStack).to.have.a.nested.property('Resources.SNSTopicSlstestprojecttopic'), 81 | expect(aliasStack).to.have.a.nested.property('Resources.SNSTopicSubscriptionSlstestprojecttopic'), 82 | expect(aliasStack).to.have.a.nested.property('Resources.Testfct1LambdaPermissionSlstestprojecttopicSNS'), 83 | ])); 84 | }); 85 | 86 | it('should replace function with alias reference', () => { 87 | serverless.service.provider.compiledCloudFormationTemplate = snsStack1; 88 | const aliasStack = serverless.service.provider.compiledCloudFormationAliasTemplate = aliasStack1; 89 | return expect(awsAlias.aliasHandleSNSEvents({}, [], {})).to.be.fulfilled 90 | .then(() => expect(aliasStack).to.have.a.nested.property('Resources.SNSTopicSlstestprojecttopic') 91 | .that.has.a.nested.property('Properties.Subscription[0].Endpoint') 92 | .that.deep.equals({ Ref: 'Testfct1Alias' }) 93 | ); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/updateAliasStack.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Unit tests for createAliasStack.. 4 | */ 5 | 6 | const { getInstalledPathSync } = require('get-installed-path'); 7 | const BbPromise = require('bluebird'); 8 | const chai = require('chai'); 9 | const sinon = require('sinon'); 10 | const path = require('path'); 11 | const AWSAlias = require('../index'); 12 | 13 | const serverlessPath = getInstalledPathSync('serverless', { local: true }); 14 | const AwsProvider = require(`${serverlessPath}/lib/plugins/aws/provider/awsProvider`); 15 | const Serverless = require(`${serverlessPath}/lib/Serverless`); 16 | 17 | chai.use(require('chai-as-promised')); 18 | chai.use(require('sinon-chai')); 19 | const expect = chai.expect; 20 | 21 | describe('updateAliasStack', () => { 22 | let serverless; 23 | let options; 24 | let awsAlias; 25 | // Sinon and stubs for SLS CF access 26 | let sandbox; 27 | let providerRequestStub; 28 | let monitorStackStub; 29 | let logStub; 30 | 31 | before(() => { 32 | sandbox = sinon.createSandbox(); 33 | }); 34 | 35 | beforeEach(() => { 36 | serverless = new Serverless(); 37 | options = { 38 | alias: 'myAlias', 39 | stage: 'myStage', 40 | region: 'us-east-1', 41 | }; 42 | serverless.setProvider('aws', new AwsProvider(serverless, options)); 43 | serverless.cli = new serverless.classes.CLI(serverless); 44 | serverless.service.service = 'testService'; 45 | serverless.service.provider.compiledCloudFormationAliasTemplate = {}; 46 | serverless.service.package.artifactDirectoryName = 'myDirectory'; 47 | awsAlias = new AWSAlias(serverless, options); 48 | providerRequestStub = sandbox.stub(awsAlias._provider, 'request'); 49 | monitorStackStub = sandbox.stub(awsAlias, 'monitorStack'); 50 | logStub = sandbox.stub(serverless.cli, 'log'); 51 | awsAlias.bucketName = 'myBucket'; 52 | 53 | logStub.returns(); 54 | }); 55 | 56 | afterEach(() => { 57 | sandbox.restore(); 58 | }); 59 | 60 | describe('#createAliasFallback()', () => { 61 | it('Should call CF with correct default parameters', () => { 62 | const expectedCFData = { 63 | StackName: 'testService-myStage-myAlias', 64 | Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], 65 | OnFailure: 'DELETE', 66 | Parameters: [], 67 | Tags: [ 68 | { Key: 'STAGE', Value: 'myStage' }, 69 | { Key: 'ALIAS', Value: 'myAlias' } 70 | ], 71 | TemplateURL: 'https://s3.amazonaws.com/myBucket/myDirectory/compiled-cloudformation-template-alias.json', 72 | }; 73 | const requestResult = { 74 | status: 'ok' 75 | }; 76 | providerRequestStub.returns(BbPromise.resolve(requestResult)); 77 | monitorStackStub.returns(BbPromise.resolve()); 78 | 79 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = {}; 80 | 81 | return expect(awsAlias.validate()).to.be.fulfilled 82 | .then(() => expect(awsAlias.createAliasFallback()).to.be.fulfilled) 83 | .then(() => BbPromise.all([ 84 | expect(providerRequestStub).to.have.been.calledOnce, 85 | expect(monitorStackStub).to.have.been.calledOnce, 86 | expect(providerRequestStub).to.have.been 87 | .calledWithExactly('CloudFormation', 'createStack', expectedCFData, 'myStage', 'us-east-1'), 88 | expect(monitorStackStub).to.have.been 89 | .calledWithExactly('create', requestResult) 90 | ])); 91 | }); 92 | 93 | it('should set stack tags', () => { 94 | const expectedCFData = { 95 | StackName: 'testService-myStage-myAlias', 96 | Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], 97 | OnFailure: 'DELETE', 98 | Parameters: [], 99 | Tags: [ 100 | { Key: 'STAGE', Value: 'myStage' }, 101 | { Key: 'ALIAS', Value: 'myAlias' }, 102 | { Key: 'tag1', Value: 'application'}, 103 | { Key: 'tag2', Value: 'component' } 104 | ], 105 | TemplateURL: 'https://s3.amazonaws.com/myBucket/myDirectory/compiled-cloudformation-template-alias.json', 106 | }; 107 | providerRequestStub.returns(BbPromise.resolve("done")); 108 | monitorStackStub.returns(BbPromise.resolve()); 109 | 110 | serverless.service.provider.stackTags = { 111 | tag1: 'application', 112 | tag2: 'component' 113 | }; 114 | 115 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = {}; 116 | 117 | return expect(awsAlias.validate()).to.be.fulfilled 118 | .then(() => expect(awsAlias.createAliasFallback()).to.be.fulfilled) 119 | .then(() => BbPromise.all([ 120 | expect(providerRequestStub).to.have.been.calledOnce, 121 | expect(providerRequestStub).to.have.been 122 | .calledWithExactly('CloudFormation', 'createStack', expectedCFData, 'myStage', 'us-east-1'), 123 | ])); 124 | }); 125 | 126 | it('should use CFN role', () => { 127 | const expectedCFData = { 128 | StackName: 'testService-myStage-myAlias', 129 | Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], 130 | OnFailure: 'DELETE', 131 | Parameters: [], 132 | Tags: [ 133 | { Key: 'STAGE', Value: 'myStage' }, 134 | { Key: 'ALIAS', Value: 'myAlias' }, 135 | ], 136 | RoleARN: 'myRole', 137 | TemplateURL: 'https://s3.amazonaws.com/myBucket/myDirectory/compiled-cloudformation-template-alias.json', 138 | }; 139 | providerRequestStub.returns(BbPromise.resolve("done")); 140 | monitorStackStub.returns(BbPromise.resolve()); 141 | 142 | serverless.service.provider.cfnRole = 'myRole'; 143 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = {}; 144 | 145 | return expect(awsAlias.validate()).to.be.fulfilled 146 | .then(() => expect(awsAlias.createAliasFallback()).to.be.fulfilled) 147 | .then(() => BbPromise.all([ 148 | expect(providerRequestStub).to.have.been.calledOnce, 149 | expect(providerRequestStub).to.have.been 150 | .calledWithExactly('CloudFormation', 'createStack', expectedCFData, 'myStage', 'us-east-1'), 151 | ])); 152 | }); 153 | 154 | it('should reject with CF error', () => { 155 | providerRequestStub.returns(BbPromise.reject(new Error('CF failed'))); 156 | monitorStackStub.returns(BbPromise.resolve()); 157 | 158 | return expect(awsAlias.createAliasFallback()).to.be.rejectedWith('CF failed') 159 | .then(() => expect(providerRequestStub).to.have.been.calledOnce); 160 | }); 161 | }); 162 | 163 | describe('#updateAlias()', () => { 164 | it('Should call CF with correct default parameters', () => { 165 | const expectedCFData = { 166 | StackName: 'testService-myStage-myAlias', 167 | Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], 168 | Parameters: [], 169 | Tags: [ 170 | { Key: 'STAGE', Value: 'myStage' }, 171 | { Key: 'ALIAS', Value: 'myAlias' } 172 | ], 173 | TemplateURL: 'https://s3.amazonaws.com/myBucket/myDirectory/compiled-cloudformation-template-alias.json', 174 | }; 175 | const requestResult = { 176 | status: 'ok' 177 | }; 178 | providerRequestStub.returns(BbPromise.resolve(requestResult)); 179 | monitorStackStub.returns(BbPromise.resolve()); 180 | 181 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = {}; 182 | 183 | return expect(awsAlias.validate()).to.be.fulfilled 184 | .then(() => expect(awsAlias.updateAlias()).to.be.fulfilled) 185 | .then(() => BbPromise.all([ 186 | expect(providerRequestStub).to.have.been.calledOnce, 187 | expect(monitorStackStub).to.have.been.calledOnce, 188 | expect(providerRequestStub).to.have.been 189 | .calledWithExactly('CloudFormation', 'updateStack', expectedCFData, 'myStage', 'us-east-1'), 190 | expect(monitorStackStub).to.have.been 191 | .calledWithExactly('update', requestResult) 192 | ])); 193 | }); 194 | 195 | it('should set stack tags', () => { 196 | const expectedCFData = { 197 | StackName: 'testService-myStage-myAlias', 198 | Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], 199 | Parameters: [], 200 | Tags: [ 201 | { Key: 'STAGE', Value: 'myStage' }, 202 | { Key: 'ALIAS', Value: 'myAlias' }, 203 | { Key: 'tag1', Value: 'application'}, 204 | { Key: 'tag2', Value: 'component' } 205 | ], 206 | TemplateURL: 'https://s3.amazonaws.com/myBucket/myDirectory/compiled-cloudformation-template-alias.json', 207 | }; 208 | providerRequestStub.returns(BbPromise.resolve("done")); 209 | monitorStackStub.returns(BbPromise.resolve()); 210 | 211 | serverless.service.provider.stackTags = { 212 | tag1: 'application', 213 | tag2: 'component' 214 | }; 215 | 216 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = {}; 217 | 218 | return expect(awsAlias.validate()).to.be.fulfilled 219 | .then(() => expect(awsAlias.updateAlias()).to.be.fulfilled) 220 | .then(() => BbPromise.all([ 221 | expect(providerRequestStub).to.have.been.calledOnce, 222 | expect(providerRequestStub).to.have.been 223 | .calledWithExactly('CloudFormation', 'updateStack', expectedCFData, 'myStage', 'us-east-1'), 224 | ])); 225 | }); 226 | 227 | it('should use CFN role', () => { 228 | const expectedCFData = { 229 | StackName: 'testService-myStage-myAlias', 230 | Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], 231 | Parameters: [], 232 | Tags: [ 233 | { Key: 'STAGE', Value: 'myStage' }, 234 | { Key: 'ALIAS', Value: 'myAlias' }, 235 | ], 236 | RoleARN: 'myRole', 237 | TemplateURL: 'https://s3.amazonaws.com/myBucket/myDirectory/compiled-cloudformation-template-alias.json', 238 | }; 239 | providerRequestStub.returns(BbPromise.resolve("done")); 240 | monitorStackStub.returns(BbPromise.resolve()); 241 | 242 | serverless.service.provider.cfnRole = 'myRole'; 243 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = {}; 244 | 245 | return expect(awsAlias.validate()).to.be.fulfilled 246 | .then(() => expect(awsAlias.updateAlias()).to.be.fulfilled) 247 | .then(() => BbPromise.all([ 248 | expect(providerRequestStub).to.have.been.calledOnce, 249 | expect(providerRequestStub).to.have.been 250 | .calledWithExactly('CloudFormation', 'updateStack', expectedCFData, 'myStage', 'us-east-1'), 251 | ])); 252 | }); 253 | 254 | it('should reject with CF error', () => { 255 | providerRequestStub.returns(BbPromise.reject(new Error('CF failed'))); 256 | monitorStackStub.returns(BbPromise.resolve()); 257 | 258 | return expect(awsAlias.updateAlias()).to.be.rejectedWith('CF failed') 259 | .then(() => expect(providerRequestStub).to.have.been.calledOnce); 260 | }); 261 | 262 | it('should resolve in case no updates are performed', () => { 263 | providerRequestStub.returns(BbPromise.resolve("done")); 264 | monitorStackStub.rejects(new Error('No updates are to be performed.')); 265 | return expect(awsAlias.updateAlias()).to.be.fulfilled 266 | .then(() => expect(providerRequestStub).to.have.been.calledOnce); 267 | }); 268 | }); 269 | 270 | describe('#updateAliasStack()', () => { 271 | let writeAliasUpdateTemplateToDiskStub; 272 | let createAliasFallbackStub; 273 | let updateAliasStub; 274 | 275 | beforeEach(() => { 276 | writeAliasUpdateTemplateToDiskStub = sandbox.stub(awsAlias, 'writeAliasUpdateTemplateToDisk'); 277 | createAliasFallbackStub = sandbox.stub(awsAlias, 'createAliasFallback'); 278 | updateAliasStub = sandbox.stub(awsAlias, 'updateAlias'); 279 | 280 | writeAliasUpdateTemplateToDiskStub.returns(BbPromise.resolve()); 281 | createAliasFallbackStub.returns(BbPromise.resolve()); 282 | updateAliasStub.returns(BbPromise.resolve()); 283 | }); 284 | 285 | it('should write template and update stack', () => { 286 | return expect(awsAlias.updateAliasStack()).to.be.fulfilled 287 | .then(() => BbPromise.all([ 288 | expect(writeAliasUpdateTemplateToDiskStub).to.have.been.calledOnce, 289 | expect(updateAliasStub).to.have.been.calledOnce, 290 | ])); 291 | }); 292 | 293 | it('should create alias if createLater has been set', () => { 294 | awsAlias._createLater = true; 295 | 296 | return expect(awsAlias.updateAliasStack()).to.be.fulfilled 297 | .then(() => BbPromise.all([ 298 | expect(writeAliasUpdateTemplateToDiskStub).to.have.been.calledOnce, 299 | expect(createAliasFallbackStub).to.have.been.calledOnce, 300 | expect(updateAliasStub).to.not.have.been.called, 301 | ])); 302 | }); 303 | 304 | it('should resolve with noDeploy', () => { 305 | awsAlias._options.noDeploy = true; 306 | 307 | return expect(awsAlias.updateAliasStack()).to.be.fulfilled 308 | .then(() => BbPromise.all([ 309 | expect(writeAliasUpdateTemplateToDiskStub).to.have.been.calledOnce, 310 | expect(createAliasFallbackStub).to.not.have.been.called, 311 | expect(updateAliasStub).to.not.have.been.called, 312 | ])); 313 | }); 314 | }); 315 | 316 | describe('#writeAliasUpdateTemplateToDisk()', () => { 317 | let writeFileSyncStub; 318 | 319 | beforeEach(() => { 320 | writeFileSyncStub = sandbox.stub(serverless.utils, 'writeFileSync'); 321 | }); 322 | 323 | it('should write the alias template', () => { 324 | const expectedPath = path.join('path-to-service', '.serverless', 'cloudformation-template-update-alias-stack.json'); 325 | const template = {}; 326 | writeFileSyncStub.returns(); 327 | 328 | serverless.config.servicePath = 'path-to-service'; 329 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = template; 330 | 331 | return expect(awsAlias.writeAliasUpdateTemplateToDisk()).to.be.fulfilled 332 | .then(() => BbPromise.all([ 333 | expect(writeFileSyncStub).to.have.been.calledOnce, 334 | expect(writeFileSyncStub).to.have.been.calledWithExactly(expectedPath, template) 335 | ])); 336 | }); 337 | }); 338 | }); 339 | -------------------------------------------------------------------------------- /test/uploadAliasArtifacts.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Unit tests for createAliasStack.. 4 | */ 5 | 6 | const { getInstalledPathSync } = require('get-installed-path'); 7 | const BbPromise = require('bluebird'); 8 | const chai = require('chai'); 9 | const sinon = require('sinon'); 10 | const AWSAlias = require('../index'); 11 | 12 | const serverlessPath = getInstalledPathSync('serverless', { local: true }); 13 | const AwsProvider = require(`${serverlessPath}/lib/plugins/aws/provider/awsProvider`); 14 | const Serverless = require(`${serverlessPath}/lib/Serverless`); 15 | 16 | chai.use(require('chai-as-promised')); 17 | chai.use(require('sinon-chai')); 18 | const expect = chai.expect; 19 | 20 | describe('uploadAliasArtifacts', () => { 21 | let serverless; 22 | let options; 23 | let awsAlias; 24 | // Sinon and stubs for SLS CF access 25 | let sandbox; 26 | let providerRequestStub; 27 | let logStub; 28 | 29 | before(() => { 30 | sandbox = sinon.createSandbox(); 31 | }); 32 | 33 | beforeEach(() => { 34 | serverless = new Serverless(); 35 | options = { 36 | alias: 'myAlias', 37 | stage: 'myStage', 38 | region: 'us-east-1', 39 | }; 40 | serverless.setProvider('aws', new AwsProvider(serverless, options)); 41 | serverless.cli = new serverless.classes.CLI(serverless); 42 | serverless.service.service = 'testService'; 43 | serverless.service.provider.compiledCloudFormationAliasTemplate = {}; 44 | serverless.service.package.artifactDirectoryName = 'myDirectory'; 45 | awsAlias = new AWSAlias(serverless, options); 46 | providerRequestStub = sandbox.stub(awsAlias._provider, 'request'); 47 | logStub = sandbox.stub(serverless.cli, 'log'); 48 | awsAlias.bucketName = 'myBucket'; 49 | 50 | logStub.returns(); 51 | }); 52 | 53 | afterEach(() => { 54 | sandbox.restore(); 55 | }); 56 | 57 | describe('#uploadAliasCloudFormationFile()', () => { 58 | it('Should call S3 putObject with correct default parameters', () => { 59 | const expectedData = { 60 | Bucket: 'myBucket', 61 | Key: 'myDirectory/compiled-cloudformation-template-alias.json', 62 | Body: '{}', 63 | ContentType: 'application/json', 64 | }; 65 | const requestResult = { 66 | status: 'ok' 67 | }; 68 | providerRequestStub.returns(BbPromise.resolve(requestResult)); 69 | 70 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = {}; 71 | 72 | return expect(awsAlias.uploadAliasCloudFormationFile()).to.be.fulfilled 73 | .then(() => BbPromise.all([ 74 | expect(providerRequestStub).to.have.been.calledOnce, 75 | expect(providerRequestStub).to.have.been 76 | .calledWithExactly('S3', 'putObject', expectedData, 'myStage', 'us-east-1'), 77 | ])); 78 | }); 79 | 80 | it('should use SSE configuration and set all supported keys', () => { 81 | const expectedData = { 82 | Bucket: 'myBucket', 83 | Key: 'myDirectory/compiled-cloudformation-template-alias.json', 84 | Body: '{}', 85 | ContentType: 'application/json', 86 | ServerSideEncryption: true, 87 | SSEKMSKeyId: 'keyID', 88 | SSECustomerAlgorithm: 'AES', 89 | SSECustomerKey: 'key', 90 | SSECustomerKeyMD5: 'md5', 91 | }; 92 | const requestResult = { 93 | status: 'ok' 94 | }; 95 | providerRequestStub.returns(BbPromise.resolve(requestResult)); 96 | 97 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = {}; 98 | serverless.service.provider.deploymentBucketObject = { 99 | serverSideEncryption: true, 100 | sseKMSKeyId: 'keyID', 101 | sseCustomerAlgorithm: 'AES', 102 | sseCustomerKey: 'key', 103 | sseCustomerKeyMD5: 'md5', 104 | }; 105 | 106 | return expect(awsAlias.uploadAliasCloudFormationFile()).to.be.fulfilled 107 | .then(() => BbPromise.all([ 108 | expect(providerRequestStub).to.have.been.calledOnce, 109 | expect(providerRequestStub).to.have.been 110 | .calledWithExactly('S3', 'putObject', expectedData, 'myStage', 'us-east-1'), 111 | ])); 112 | }); 113 | 114 | it('should use SSE configuration and ignore all unsupported keys', () => { 115 | const expectedData = { 116 | Bucket: 'myBucket', 117 | Key: 'myDirectory/compiled-cloudformation-template-alias.json', 118 | Body: '{}', 119 | ContentType: 'application/json', 120 | ServerSideEncryption: true, 121 | SSEKMSKeyId: 'keyID', 122 | }; 123 | const requestResult = { 124 | status: 'ok' 125 | }; 126 | providerRequestStub.returns(BbPromise.resolve(requestResult)); 127 | 128 | serverless.service.provider.compiledCloudFormationAliasCreateTemplate = {}; 129 | serverless.service.provider.deploymentBucketObject = { 130 | serverSideEncryption: true, 131 | sseKMSKeyId: 'keyID', 132 | sseCustomAlgorithm: 'AES', 133 | unknown: 'key', 134 | invalid: 'md5', 135 | }; 136 | 137 | return expect(awsAlias.uploadAliasCloudFormationFile()).to.be.fulfilled 138 | .then(() => BbPromise.all([ 139 | expect(providerRequestStub).to.have.been.calledOnce, 140 | expect(providerRequestStub).to.have.been 141 | .calledWithExactly('S3', 'putObject', expectedData, 'myStage', 'us-east-1'), 142 | ])); 143 | }); 144 | 145 | it('should reject with S3 error', () => { 146 | providerRequestStub.returns(BbPromise.reject(new Error('Failed'))); 147 | 148 | return expect(awsAlias.uploadAliasCloudFormationFile()).to.be.rejectedWith('Failed') 149 | .then(() => expect(providerRequestStub).to.have.been.calledOnce); 150 | }); 151 | }); 152 | 153 | describe('#uploadAliasArtifacts()', () => { 154 | let uploadAliasCloudFormationFileStub; 155 | 156 | beforeEach(() => { 157 | uploadAliasCloudFormationFileStub = sandbox.stub(awsAlias, 'uploadAliasCloudFormationFile'); 158 | uploadAliasCloudFormationFileStub.returns(BbPromise.resolve()); 159 | }); 160 | 161 | it('should call uploadAliasCloudFormationFile', () => { 162 | return expect(awsAlias.uploadAliasArtifacts()).to.be.fulfilled 163 | .then(() => BbPromise.all([ 164 | expect(uploadAliasCloudFormationFileStub).to.have.been.calledOnce, 165 | ])); 166 | }); 167 | 168 | it('should resolve with noDeploy', () => { 169 | awsAlias._options.noDeploy = true; 170 | 171 | return expect(awsAlias.uploadAliasArtifacts()).to.be.fulfilled 172 | .then(() => BbPromise.all([ 173 | expect(uploadAliasCloudFormationFileStub).to.not.have.been.called, 174 | ])); 175 | }); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Unit tests for utils. 4 | */ 5 | const _ = require('lodash'); 6 | const chai = require('chai'); 7 | const Utils = require('../lib/utils'); 8 | 9 | chai.use(require('chai-subset')); 10 | const expect = chai.expect; 11 | 12 | describe('Utils', function() { 13 | describe('#findReferences()', () => { 14 | it('should not fail without args', () => { 15 | expect(Utils.findReferences()).to.deep.equal([]); 16 | }); 17 | 18 | it('should not fail on invalid root', () => { 19 | expect(Utils.findReferences(null)).to.deep.equal([]); 20 | }); 21 | 22 | it('should not fail on invalid references', () => { 23 | const testRoot = {}; 24 | expect(Utils.findReferences(testRoot)).to.deep.equal([]); 25 | }); 26 | 27 | it('should return CF Refs', () => { 28 | const testRoot = { 29 | items: [ 30 | { 31 | testItem: { 32 | name: 'Ref' 33 | } 34 | }, 35 | { 36 | otherTestItem: { 37 | prop1: { 38 | arrayTest: [ 39 | { 40 | Ref: 'Ref#2' 41 | } 42 | ] 43 | }, 44 | prop2: { 45 | Ref: 'Ref#3' 46 | } 47 | } 48 | } 49 | ], 50 | other: { 51 | Ref: 'Ref#4' 52 | } 53 | }; 54 | expect(Utils.findReferences(testRoot, [ 'Ref#2', 'Ref#3' ])) 55 | .to.deep.equal([ 'items[1].otherTestItem.prop2', 'items[1].otherTestItem.prop1.arrayTest[0]' ]); 56 | }); 57 | 58 | it('should return CF GetAtts', () => { 59 | const testRoot = { 60 | items: [ 61 | { 62 | testItem: { 63 | 'Fn::GetAtt': [ 64 | 'Ref', 65 | 'Prop' 66 | ] 67 | } 68 | }, 69 | { 70 | otherTestItem: { 71 | prop1: { 72 | arrayTest: [ 73 | { 74 | 'Fn::GetAtt': [ 75 | 'Ref#2', 76 | 'Prop#2' 77 | ] 78 | } 79 | ] 80 | }, 81 | prop2: { 82 | 'Fn::GetAtt': [ 83 | 'Ref#3', 84 | 'Prop#3' 85 | ] 86 | } 87 | } 88 | } 89 | ], 90 | other: { 91 | 'Fn::GetAtt': [ 92 | 'Ref#4', 93 | 'Prop#4' 94 | ] 95 | } 96 | }; 97 | expect(Utils.findReferences(testRoot, [ 'Ref#4', 'Ref#3' ])) 98 | .to.deep.equal([ 'other', 'items[1].otherTestItem.prop2' ]); 99 | }); 100 | 101 | it('should succeed without given refs', () => { 102 | const testRoot = { 103 | items: [ 104 | { 105 | testItem: { 106 | name: 'Ref' 107 | } 108 | }, 109 | { 110 | otherTestItem: { 111 | prop1: { 112 | arrayTest: [ 113 | { 114 | Ref: 'Ref#2' 115 | } 116 | ] 117 | }, 118 | prop2: { 119 | Ref: 'Ref#3' 120 | } 121 | } 122 | } 123 | ], 124 | other: { 125 | Ref: 'Ref#4' 126 | } 127 | }; 128 | expect(Utils.findReferences(testRoot, [])) 129 | .to.deep.equal([]); 130 | }); 131 | }); 132 | 133 | describe('#findAllReferences()', () => { 134 | it('should not fail without args', () => { 135 | expect(Utils.findAllReferences()).to.deep.equal([]); 136 | }); 137 | 138 | it('should not fail on invalid root', () => { 139 | expect(Utils.findAllReferences(null)).to.deep.equal([]); 140 | }); 141 | 142 | it('should find all CF refs', () => { 143 | const testRoot = { 144 | items: [ 145 | { 146 | testItem: { 147 | name: 'Ref' 148 | } 149 | }, 150 | { 151 | otherTestItem: { 152 | prop1: { 153 | arrayTest: [ 154 | { 155 | Ref: 'Ref#2' 156 | } 157 | ] 158 | }, 159 | prop2: { 160 | Ref: 'Ref#3' 161 | } 162 | } 163 | } 164 | ], 165 | other: { 166 | Ref: 'Ref#4' 167 | } 168 | }; 169 | expect(Utils.findAllReferences(testRoot)) 170 | .to.deep.equal([ 171 | { 172 | 'path': 'other', 173 | 'ref': 'Ref#4' 174 | }, 175 | { 176 | 'path': 'items[1].otherTestItem.prop2', 177 | 'ref': 'Ref#3' 178 | }, 179 | { 180 | 'path': 'items[1].otherTestItem.prop1.arrayTest[0]', 181 | 'ref': 'Ref#2' 182 | } 183 | ]); 184 | }); 185 | 186 | it('should find all CF GetAtts', () => { 187 | const testRoot = { 188 | items: [ 189 | { 190 | testItem: { 191 | 'Fn::GetAtt': [ 192 | 'Ref', 193 | 'Prop' 194 | ] 195 | } 196 | }, 197 | { 198 | otherTestItem: { 199 | prop1: { 200 | arrayTest: [ 201 | { 202 | 'Fn::GetAtt': [ 203 | 'Ref#2', 204 | 'Prop#2' 205 | ] 206 | } 207 | ] 208 | }, 209 | prop2: { 210 | 'Fn::GetAtt': [ 211 | 'Ref#3', 212 | 'Prop#3' 213 | ] 214 | } 215 | } 216 | } 217 | ], 218 | other: { 219 | 'Fn::GetAtt': [ 220 | 'Ref#4', 221 | 'Prop#4' 222 | ] 223 | } 224 | }; 225 | expect(Utils.findAllReferences(testRoot)) 226 | .to.deep.equal([ 227 | { 228 | 'path': 'other', 229 | 'ref': 'Ref#4' 230 | }, 231 | { 232 | 'path': 'items[1].otherTestItem.prop2', 233 | 'ref': 'Ref#3' 234 | }, 235 | { 236 | 'path': 'items[1].otherTestItem.prop1.arrayTest[0]', 237 | 'ref': 'Ref#2' 238 | }, 239 | { 240 | 'path': 'items[0].testItem', 241 | 'ref': 'Ref' 242 | } 243 | ]); 244 | }); 245 | }); 246 | 247 | describe('#normalizeAliasForLogicalId()', () => { 248 | it('should do nothing if alias is compliant or nil', () => { 249 | const values = [ 250 | 'aValidAlias', 251 | 'myAlias0123', 252 | null, 253 | undefined 254 | ]; 255 | _.forEach(values, value => { 256 | expect(Utils.normalizeAliasForLogicalId(value)).to.equal(value); 257 | }); 258 | }); 259 | 260 | it('should throw on invalid characters', () => { 261 | const values = [ 262 | 'aValid$Alias', 263 | 'my#Alias0123', 264 | 'n*ull', 265 | 'alias~233' 266 | ]; 267 | _.forEach(values, value => { 268 | expect(() => Utils.normalizeAliasForLogicalId(value)) 269 | .to.throw(/^Unsupported character/); 270 | }); 271 | }); 272 | 273 | it('should replace all supported characters', () => { 274 | const values = { 275 | a_Valid_Alias: 'aUscoreValidUscoreAlias', 276 | 'my-Alias0123': 'myDashAlias0123', 277 | 'a+different_one': 'aPlusdifferentUscoreone', 278 | }; 279 | _.forOwn(values, (value, alias) => { 280 | expect(Utils.normalizeAliasForLogicalId(alias)).to.equal(value); 281 | }); 282 | }); 283 | }); 284 | 285 | describe('#hasPermissionPrincipal()', () => { 286 | it('should work with string principals', () => { 287 | const permission = { 288 | 'Type': 'AWS::Lambda::Permission', 289 | 'Properties': { 290 | 'FunctionName': { 291 | 'Fn::GetAtt': [ 292 | 'MyLambdaLambdaFunction', 293 | 'Arn' 294 | ] 295 | }, 296 | 'Action': 'lambda:InvokeFunction', 297 | 'Principal': 'apigateway.amazonaws.com', 298 | 'SourceArn': { 299 | 'Fn::Join': [ 300 | '', 301 | [ 302 | 'arn:', 303 | { 304 | 'Ref': 'AWS::Partition' 305 | }, 306 | ':execute-api:', 307 | { 308 | 'Ref': 'AWS::Region' 309 | }, 310 | ':', 311 | { 312 | 'Ref': 'AWS::AccountId' 313 | }, 314 | ':', 315 | { 316 | 'Ref': 'ApiGatewayRestApi' 317 | }, 318 | '/*/*' 319 | ] 320 | ] 321 | } 322 | } 323 | }; 324 | 325 | expect(Utils.hasPermissionPrincipal(permission, 'apigateway')).to.be.true; 326 | }); 327 | 328 | it('should work with constructed principals', () => { 329 | const permission = { 330 | 'Type': 'AWS::Lambda::Permission', 331 | 'Properties': { 332 | 'FunctionName': { 333 | 'Fn::GetAtt': [ 334 | 'MyLambdaLambdaFunction', 335 | 'Arn' 336 | ] 337 | }, 338 | 'Action': 'lambda:InvokeFunction', 339 | 'Principal': { 340 | 'Fn::Join': [ 341 | '', 342 | [ 343 | 'apigateway.', 344 | { 345 | 'Ref': 'AWS::URLSuffix' 346 | } 347 | ] 348 | ] 349 | }, 350 | 'SourceArn': { 351 | 'Fn::Join': [ 352 | '', 353 | [ 354 | 'arn:', 355 | { 356 | 'Ref': 'AWS::Partition' 357 | }, 358 | ':execute-api:', 359 | { 360 | 'Ref': 'AWS::Region' 361 | }, 362 | ':', 363 | { 364 | 'Ref': 'AWS::AccountId' 365 | }, 366 | ':', 367 | { 368 | 'Ref': 'ApiGatewayRestApi' 369 | }, 370 | '/*/*' 371 | ] 372 | ] 373 | } 374 | } 375 | }; 376 | 377 | expect(Utils.hasPermissionPrincipal(permission, 'apigateway')).to.be.true; 378 | }); 379 | 380 | it ('should return false if the service is not matched', () => { 381 | const permission = { 382 | 'Type': 'AWS::Lambda::Permission', 383 | 'Properties': { 384 | 'FunctionName': { 385 | 'Fn::GetAtt': [ 386 | 'MyLambdaLambdaFunction', 387 | 'Arn' 388 | ] 389 | }, 390 | 'Action': 'lambda:InvokeFunction', 391 | 'Principal': { 392 | 'Fn::Join': [ 393 | '', 394 | [ 395 | 'apigateway.', 396 | { 397 | 'Ref': 'AWS::URLSuffix' 398 | } 399 | ] 400 | ] 401 | }, 402 | 'SourceArn': { 403 | 'Fn::Join': [ 404 | '', 405 | [ 406 | 'arn:', 407 | { 408 | 'Ref': 'AWS::Partition' 409 | }, 410 | ':execute-api:', 411 | { 412 | 'Ref': 'AWS::Region' 413 | }, 414 | ':', 415 | { 416 | 'Ref': 'AWS::AccountId' 417 | }, 418 | ':', 419 | { 420 | 'Ref': 'ApiGatewayRestApi' 421 | }, 422 | '/*/*' 423 | ] 424 | ] 425 | } 426 | } 427 | }; 428 | 429 | expect(Utils.hasPermissionPrincipal(permission, 'events')).to.be.false; 430 | }); 431 | }); 432 | }); 433 | -------------------------------------------------------------------------------- /test/validate.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Unit tests for validate. 4 | */ 5 | 6 | const { getInstalledPathSync } = require('get-installed-path'); 7 | const BbPromise = require('bluebird'); 8 | const chai = require('chai'); 9 | const AWSAlias = require('../index'); 10 | 11 | const serverlessPath = getInstalledPathSync('serverless', { local: true }); 12 | const AwsProvider = require(`${serverlessPath}/lib/plugins/aws/provider/awsProvider`); 13 | const Serverless = require(`${serverlessPath}/lib/Serverless`); 14 | 15 | chai.use(require('chai-as-promised')); 16 | const expect = chai.expect; 17 | 18 | describe('#validate()', () => { 19 | let serverless; 20 | let options; 21 | let awsAlias; 22 | 23 | beforeEach(() => { 24 | serverless = new Serverless(); 25 | options = { 26 | stage: 'myStage', 27 | region: 'us-east-1', 28 | }; 29 | serverless.service.service = 'myService'; 30 | serverless.setProvider('aws', new AwsProvider(serverless, options)); 31 | serverless.cli = new serverless.classes.CLI(serverless); 32 | awsAlias = new AWSAlias(serverless, options); 33 | }); 34 | 35 | it('should fail with old Serverless version', () => { 36 | serverless.version = '1.6.0'; 37 | return expect(awsAlias.validate()).to.be.rejectedWith('must be >= 1.12.0'); 38 | }); 39 | 40 | it('should succeed with Serverless version 1.12.0', () => { 41 | serverless.version = '1.12.0'; 42 | return expect(awsAlias.validate()).to.eventually.be.fulfilled; 43 | }); 44 | 45 | it('should succeed with Serverless version 1.13.0', () => { 46 | serverless.version = '1.13.0'; 47 | return expect(awsAlias.validate()).to.eventually.be.fulfilled; 48 | }); 49 | 50 | it('should initialize the plugin with options', () => { 51 | return expect(awsAlias.validate()).to.eventually.be.fulfilled 52 | .then(() => BbPromise.all([ 53 | expect(awsAlias).to.have.property('_stage', 'myStage'), 54 | expect(awsAlias).to.have.property('_alias', 'myStage'), 55 | expect(awsAlias).to.have.property('_stackName', 'myService-myStage'), 56 | ])); 57 | }); 58 | 59 | it('should set SERVERLESS_ALIAS', () => { 60 | return expect(awsAlias.validate()).to.eventually.be.fulfilled 61 | .then(() => expect(process.env.SERVERLESS_ALIAS).to.equal('myStage')); 62 | }); 63 | 64 | it('should succeed', () => { 65 | return expect(awsAlias.validate()).to.eventually.be.fulfilled; 66 | }); 67 | }); 68 | --------------------------------------------------------------------------------