├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── docs ├── configure.md ├── deploy.md ├── previewUpdate.md └── update.md ├── examples ├── ec2DeployFailure.js ├── ec2DeploySuccess.js ├── ec2PreviewUpdate.js ├── ec2UpdateFailure.js ├── ec2UpdateSuccess.js ├── lib │ ├── ec2DeployBase.js │ ├── ec2PreviewUpdateBase.js │ └── ec2UpdateBase.js └── templates │ └── ec2.json ├── index.js ├── lib ├── cloudFormation.js ├── cloudFormationOperation.js ├── configValidator.js ├── constants.js ├── deploy.js ├── previewUpdate.js ├── update.js └── utilities.js ├── package-lock.json ├── package.json └── test ├── .eslintrc ├── index.spec.js ├── lib ├── cloudFormation.spec.js ├── cloudFormationOperation.spec.js ├── configValidator.spec.js ├── deploy.spec.js ├── previewUpdate.spec.js ├── update.spec.js └── utilities.spec.js ├── mochaInit.js └── resources └── index.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "brace-style": [2, "stroustrup", { "allowSingleLine": true }], 7 | "consistent-return": 0, 8 | "curly": 2, 9 | "eqeqeq": 2, 10 | "indent": [2, 2, { "SwitchCase": 1 }], 11 | "key-spacing": [2, { 12 | "beforeColon": false, 13 | "afterColon": true 14 | }], 15 | keyword-spacing: [2, { 16 | "before": true, 17 | "after": true 18 | }], 19 | "no-multiple-empty-lines": 2, 20 | "no-throw-literal": 2, 21 | "no-underscore-dangle": 0, 22 | "no-unused-vars": [2, { 23 | "vars": "all", 24 | "args": "none" 25 | }], 26 | "no-use-before-define": [2, "nofunc"], 27 | "object-curly-spacing": [2, "always"], 28 | "quote-props": [2, "as-needed"], 29 | "quotes": [2, "single"], 30 | "radix": 2, 31 | "space-before-blocks": [2, "always"], 32 | "space-before-function-paren": [2, "always"], 33 | "space-in-parens": [2, "never"], 34 | "space-unary-ops": [2, { 35 | "words": true, 36 | "nonwords": false 37 | }], 38 | "strict": [2, "never"], 39 | "wrap-iife": [2, "inside"] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Sublime Text 2 | *sublime-* 3 | 4 | # Node, NPM 5 | **/node_modules 6 | npm* 7 | 8 | # TODO lists. 9 | TODO* 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.5.0 4 | 5 | * Add an update preview via changesets. 6 | 7 | ## 0.4.0 8 | 9 | * Add the ability to run stack updates. 10 | * Improve the error messages for stack operation failure. 11 | * Bump minimum Node.js version to something more modern; barely needed, but 12 | better safe than sorry. 13 | 14 | ## 0.3.2 15 | 16 | * Add capabilities to the configuration options. 17 | 18 | ## 0.3.1 19 | 20 | * Update aws-sdk package version. 21 | 22 | ## 0.3.0 23 | 24 | * Add more detail to failure mode documentation. 25 | * Ensure that concurrently running deployments in the same process work. 26 | 27 | ## 0.2.2 28 | 29 | * The minor improvement turned out to disable client options. So fix that. 30 | 31 | ## 0.2.1 32 | 33 | * Minor improvement to configuration validation. 34 | 35 | ## 0.2.0 36 | 37 | * Lazy creation of AWS client. 38 | * Add AWS client options to the main configuration for those who want them. 39 | 40 | ## 0.1.1 41 | 42 | * Update package versions, particularly ESLint due to a rule syntax change. 43 | 44 | ## 0.1.0 45 | 46 | * Initial release. 47 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Definition file for grunt tasks. 3 | */ 4 | 5 | // Core. 6 | var path = require('path'); 7 | 8 | module.exports = function (grunt) { 9 | 10 | // Always output stack traces. 11 | grunt.option('stack', true); 12 | 13 | grunt.initConfig({ 14 | eslint: { 15 | target: [ 16 | '**/*.js', 17 | '!**/node_modules/**' 18 | ] 19 | }, 20 | 21 | mochaTest: { 22 | test: { 23 | options: { 24 | reporter: 'spec', 25 | quiet: false, 26 | clearRequireCache: false, 27 | require: [ 28 | path.join(__dirname, 'test/mochaInit.js') 29 | ] 30 | }, 31 | src: [ 32 | 'test/**/*.spec.js' 33 | ] 34 | } 35 | } 36 | }); 37 | 38 | grunt.loadNpmTasks('grunt-eslint'); 39 | grunt.loadNpmTasks('grunt-mocha-test'); 40 | 41 | grunt.registerTask('test', [ 42 | 'eslint', 43 | 'mochaTest' 44 | ]); 45 | }; 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015 Reason [reason -A- exratione.com] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudFormation Deploy 2 | 3 | This package provides a simple programmatic interface for the deployment or 4 | update of a CloudFormation stack. 5 | 6 | ## Installation 7 | 8 | Obtain CloudFormation Deploy via NPM: 9 | 10 | ``` 11 | npm install cloudformation-deploy 12 | ``` 13 | 14 | ## Examples 15 | 16 | See the [examples directory][1] for ready to run examples. 17 | 18 | ## How to Use CloudFormation Deploy 19 | 20 | * [Configuring AWS Credentials](./docs/configure.md) 21 | * [Creating or Replacing a CloudFormation Stack](./docs/deploy.md) 22 | * [Preview the Update of a CloudFormation Stack](./docs/previewUpdate.md) 23 | * [Updating a CloudFormation Stack](./docs/update.md) 24 | 25 | [1]: ./examples 26 | -------------------------------------------------------------------------------- /docs/configure.md: -------------------------------------------------------------------------------- 1 | # Configuring AWS Credentials 2 | 3 | For deployment to work, suitable credentials for the destination AWS account 4 | must be present. The credentials must at a minimum allow interaction with 5 | CloudFormation stacks and granting all suitable permissions to stack resources 6 | via IAM roles. 7 | 8 | To make credentials available, either create a credentials file in the standard 9 | location, set environment variables to hold the key, secret key and region, or 10 | run on an EC2 instance with an IAM role that has suitable permissions. These 11 | options are [described in the AWS SDK documentation][1]. 12 | 13 | For example, if using environment variables: 14 | 15 | ```bash 16 | export AWS_ACCESS_KEY_ID= 17 | export AWS_SECRET_ACCESS_KEY= 18 | export AWS_REGION=us-east-1 19 | ``` 20 | 21 | Alternatively, CloudFormation Deploy accepts an optional configuration object 22 | that is passed to the AWS SDK CloudFormation client instance if present. 23 | This is not recommended: it is bad practice to specify access keys in code or 24 | configuration for code. You should always add them to the environment in one of 25 | the ways noted above. 26 | 27 | [1]: http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/node-configuring.html 28 | -------------------------------------------------------------------------------- /docs/deploy.md: -------------------------------------------------------------------------------- 1 | # Creating or Replacing a CloudFormation Stack 2 | 3 | This package implements the following pattern of stack creation and deletion in 4 | order to run either the creation of a new stack or the replacement of an 5 | existing stack: 6 | 7 | * Request stack creation. 8 | * Wait on the stack status to show show success. 9 | * Update existing resources to point to the new stack. 10 | * Delete any previous instances of the stack once the switchover is done. 11 | 12 | This is well suited to, for example, the deployment an Auto Scaling Group for 13 | webservers that attaches to an existing Elastic Load Balancer. In this way 14 | updates can be seamless: the new servers are added, the old removed, without 15 | any interruption of traffic. 16 | 17 | In general it is a good idea to keep long-lasting resources such as an Elastic 18 | Load Balancer or Route 53 DNS entry in their own stack, separate from those 19 | resources that will be frequently updated. References to the long-lasting 20 | resources can be passed into the deployed stack via parameters, or connections 21 | can be made via a function passed in to the stack deployment process. 22 | 23 | ## Create the CloudFormation Template 24 | 25 | Generate or load the CloudFormation template. This module can accept the 26 | template as either a JSON string, an object, or a URL to a template uploaded to 27 | S3. Use the latter method for larger templates, as it has a larger maximum size 28 | limit. 29 | 30 | ## Run the Deployment 31 | 32 | Run the following code. 33 | 34 | ```js 35 | cloudFormationDeploy = require('cloudformation-deploy'); 36 | 37 | // Pull in the CloudFormation template from a JSON file or object. 38 | //var template = fs.readFileSync('example.json', { encoding: 'utf8' }); 39 | //var template = { ... }; 40 | // Or specify a URL. 41 | var template = 'http://s3.amazonaws.com/bucket/example.json'; 42 | 43 | var config = { 44 | // -------------------- 45 | // Required properties. 46 | // -------------------- 47 | 48 | // The baseName and deployId are combined to form a unique name. 49 | // 50 | // Tags are automatically added based on these values and the version, and are 51 | // used when deleting old stacks from earlier versions and prior deployments. 52 | // 53 | // The deployId property is along the lines of BUILD_NUMBER in Jenkins or 54 | // a similar values generated by another task framework. Using a Unix 55 | // timestamp is an acceptable fallback if nothing else is available. 56 | baseName: 'example-stack', 57 | deployId: '15', 58 | version: '0.1.0', 59 | 60 | // -------------------- 61 | // Optional properties. 62 | // -------------------- 63 | 64 | // If defined, this property is passed to the AWS SDK client. It is not 65 | // recommended to use this approach, see above for comments on configuring 66 | // AWS access via the environment. 67 | // clientOptions: { 68 | // accessKeyId: 'akid', 69 | // secretAccessKey: 'secret', 70 | // region: 'us-east-1' 71 | // }, 72 | 73 | // Needed for stacks that affect permissions, which is most application stacks 74 | // these days. 75 | // See: http://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html 76 | capabilities: [ 77 | cloudFormationDeploy.capabilities.CAPABILITY_IAM, 78 | cloudFormationDeploy.capabilities.CAPABILITY_NAMED_IAM 79 | ], 80 | 81 | // Timeout in minutes for the process of stack creation. 82 | createStackTimeoutInMinutes: 10, 83 | 84 | // Specify additional tags to apply to the stack. 85 | tags: { 86 | name: 'value' 87 | }, 88 | 89 | // Pass in any parameters required by the template. 90 | parameters: { 91 | name: 'value' 92 | }, 93 | 94 | // Seconds to wait between each check on the progress of stack creation or 95 | // deletion. 96 | progressCheckIntervalInSeconds: 10, 97 | 98 | // A function invoked whenever a CloudFormation event is created during 99 | // stack creation or deletion. 100 | onEventFn: function (event) { 101 | console.log(event); 102 | }, 103 | 104 | // A function invoked after the CloudFormation stack is successfully created 105 | // but before any prior stack is deleted. This allows for a clean switchover 106 | // of resources to use the new stack. 107 | // 108 | // The provided stackDescription object is the standard output from the 109 | // describeStacks API for the created stack. 110 | postCreationFn: function (stackDescription, callback) { 111 | 112 | // Take action here, such as updating a Route 53 DNS entry and waiting for 113 | // propagation to complete. 114 | 115 | callback(); 116 | }, 117 | 118 | // What to do with prior instances of this stack, which is defined as any 119 | // stack deployed with the same baseName. 120 | priorInstance: cloudFormationDeploy.priorInstance.DELETE, 121 | 122 | // What to do with this deployed stack should it fail to complete 123 | // successfully. 124 | onDeployFailure: cloudFormationDeploy.onDeployFailure.DELETE 125 | }; 126 | 127 | cloudFormationDeploy.deploy(config, template, function (error, results) { 128 | if (error) { 129 | console.error(error); 130 | } 131 | 132 | // Whether or not there is an error, the results object is returned. It will 133 | // usually have additional useful information on why the stack deployment 134 | // failed. On success it will include the stack description, outputs 135 | // defined in the CloudFormation template, and events. 136 | console.log(results); 137 | }); 138 | ``` 139 | 140 | ## Deployment Configuration 141 | 142 | The `config` object passed to `cloudFormationDeploy.deploy` supports the 143 | following required and optional properties. 144 | 145 | ### Required Properties 146 | 147 | `baseName` - `string` - Combined with `deployId` to generat the stack name. 148 | 149 | `version` - `string` - The version of the application deployed to the stack. 150 | 151 | `deployId` - `string` - A distinguishing string for this stack baseName, such as 152 | a build number, to ensure this stack name is unique even for multiple 153 | deployments of the same version. 154 | 155 | ### Optional Properties 156 | 157 | `createStackTimeoutInMinutes` - `number` - A timeout in minutes for stack 158 | creation or deletion. Defaults to `10`. 159 | 160 | `tags` - `object` - The tags to apply to the stack in addition to those created 161 | automatically based on the `baseName`. Tag values must be strings. 162 | 163 | `parameters` - `object` - Values to apply to the parameters in the 164 | CloudFormation template. Parameter values must be strings. 165 | 166 | `progressCheckIntervalInSeconds` - `number` - Number of seconds to wait between each 167 | check on the progress of stack creation or deletion. Defaults to `10`. 168 | 169 | `onEventFn` - `function` - A function invoked whenever a new event is 170 | created during stack creation or deletion. 171 | 172 | ``` 173 | function (event) { 174 | console.log(event); 175 | } 176 | ``` 177 | 178 | `postCreationFunction` - `function` - A function invoked after a stack is 179 | successfully created but before any prior instance of the stack with the same 180 | `baseName` is destroyed. This allows resources to be updated to use the new 181 | stack. 182 | 183 | ``` 184 | function (stackDescription, callback) { 185 | 186 | // Take action here, such as updating a Route 53 DNS entry and waiting for 187 | // propagation to complete. 188 | 189 | callback(); 190 | } 191 | ``` 192 | 193 | `priorInstance` - `string` - One of the allowed values describing what to do 194 | with previously deployed stacks with the same `baseName`. Defaults to 195 | `cloudFormationDeploy.priorInstance.DELETE` and the allowed values are: 196 | 197 | * `cloudFormationDeploy.priorInstance.DELETE` 198 | * `cloudFormationDeploy.priorInstance.DO_NOTHING` 199 | 200 | `onDeployFailure` - `string` - One of the allowed values describing what to do with 201 | the deployed stack on failure. Defaults to 202 | `cloudFormationDeploy.onDeployFailure.DELETE` and the allowed values are: 203 | 204 | * `cloudFormationDeploy.onDeployFailure.DELETE` 205 | * `cloudFormationDeploy.onDeployFailure.DO_NOTHING` 206 | 207 | ## Failure Cases 208 | 209 | All failure cases will result in `cloudFormationDeploy.deploy` calling back with 210 | an error. The state of the stacks depends on where in the process that error 211 | occurred, however. 212 | 213 | 1) If the stack creation fails, then: 214 | 215 | * The `postCreationFn` function is not invoked. 216 | * The failed stack is deleted if `priorInstance` is 217 | `cloudFormationDeploy.onDeployFailure.DELETE`. 218 | * Any prior stacks are left untouched. 219 | 220 | 2) If the stack creation succeeds, but subsequent calls to obtain the stack 221 | details fail, then: 222 | 223 | * The `postCreationFn` function is not invoked. 224 | * The newly created stack is not deleted. 225 | * Any prior stacks are left untouched. 226 | 227 | 3) If the `postCreationFn` calls back with an error due to failure, then: 228 | 229 | * The newly created stack is not deleted. 230 | * Any prior stacks are left untouched. 231 | 232 | 4) If the deletion of prior stacks fails, then: 233 | 234 | * The newly created stack is not deleted. 235 | * Any prior stacks are left in whatever state the deletion failure leaves them 236 | in. 237 | -------------------------------------------------------------------------------- /docs/previewUpdate.md: -------------------------------------------------------------------------------- 1 | # Preview the Update of a CloudFormation Stack 2 | 3 | The [Change Set functionality][1] makes it possible to explore the consequences 4 | of an update before trying it out for real, at least to some degree. There are 5 | certainly many ways to create an update that a Change Set will declare useful, 6 | but that will nonetheless fail horribly in reality. 7 | 8 | ## Amend the CloudFormation Template 9 | 10 | Edit the CloudFormation template originally used to create the stack, to make 11 | the necessary changes. It is also possible to run an update in which only the 12 | parameters passed to the template change. This module can accept the template as 13 | either a JSON string, an object, or a URL to a template uploaded to S3. Use the 14 | latter method for larger templates, as it has a larger maximum size limit. 15 | 16 | ## Run the Preview 17 | 18 | Run the following code. 19 | 20 | ```js 21 | cloudFormationDeploy = require('cloudformation-deploy'); 22 | 23 | // Pull in the CloudFormation template from a JSON file or object. 24 | //var template = fs.readFileSync('example.json', { encoding: 'utf8' }); 25 | //var template = { ... }; 26 | // Or specify a URL. 27 | var template = 'http://s3.amazonaws.com/bucket/example.json'; 28 | 29 | var config = { 30 | // -------------------- 31 | // Required properties. 32 | // -------------------- 33 | 34 | // The name of the Change Set to be created. 35 | changeSetName: 'example-stack-15-changeset-1', 36 | 37 | // The name of the stack to be updated. 38 | stackName: 'example-stack-15', 39 | 40 | // -------------------- 41 | // Optional properties. 42 | // -------------------- 43 | 44 | // If defined, this property is passed to the AWS SDK client. It is not 45 | // recommended to use this approach, see above for comments on configuring 46 | // AWS access via the environment. 47 | // clientOptions: { 48 | // accessKeyId: 'akid', 49 | // secretAccessKey: 'secret', 50 | // region: 'us-east-1' 51 | // }, 52 | 53 | // Needed for stacks that affect permissions, which is most application stacks 54 | // these days. 55 | // See: http://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html 56 | capabilities: [ 57 | cloudFormationDeploy.capabilities.CAPABILITY_IAM, 58 | cloudFormationDeploy.capabilities.CAPABILITY_NAMED_IAM 59 | ], 60 | 61 | // Specify additional tags to apply to the stack. 62 | tags: { 63 | name: 'value' 64 | }, 65 | 66 | // Pass in any parameters required by the template. 67 | parameters: { 68 | name: 'value' 69 | }, 70 | 71 | // Seconds to wait between each check on the progress of stack creation or 72 | // deletion. 73 | progressCheckIntervalInSeconds: 10, 74 | 75 | // If true, delete the Change Set after obtaining the information it provides. 76 | deleteChangeSet: true 77 | }; 78 | 79 | cloudFormationDeploy.previewUpdate(config, template, function (error, results) { 80 | if (error) { 81 | console.error(error); 82 | } 83 | 84 | // Whether or not there is an error, the results object is returned. It will 85 | // usually have additional useful information, including the details of the 86 | // proposed update: which operations will occur, and whether or not the 87 | // update is expected to succeed or fail. 88 | console.log(results); 89 | }); 90 | ``` 91 | 92 | ## Preview Update Configuration 93 | 94 | The `config` object passed to `cloudFormationDeploy.update` supports the 95 | following required and optional properties. 96 | 97 | ### Required Properties 98 | 99 | `changeSetName` - `string` - The name of the Change Set to create. 100 | 101 | `stackName` - `string` - The name of the stack to update. 102 | 103 | ### Optional Properties 104 | 105 | `tags` - `object` - The tags to apply to the stack in addition to those created 106 | automatically based on the `baseName`. Tag values must be strings. 107 | 108 | `parameters` - `object` - Values to apply to the parameters in the 109 | CloudFormation template. Parameter values must be strings. 110 | 111 | `progressCheckIntervalInSeconds` - `number` - Number of seconds to wait between each 112 | check on the progress of stack creation or deletion. Defaults to `10`. 113 | 114 | `deleteChangeSet` - `boolean` - If true, clean up by deleting the Change Set 115 | after obtaining its information. 116 | 117 | ## Failure Cases 118 | 119 | Failure to create or obtain information from the Change Set will result in 120 | `cloudFormationDeploy.previewUpdate` calling back with an error. If a Change Set 121 | is successfully created prior to the point of failure, it will not be deleted. 122 | 123 | [1]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html 124 | -------------------------------------------------------------------------------- /docs/update.md: -------------------------------------------------------------------------------- 1 | # Updating a CloudFormation Stack 2 | 3 | Updating an existing stack is fairly easy, assuming that you understand exactly 4 | what will happen beforehand. It is a very good idea to use the 5 | [Change Set functionality][1] to explore the consequences of an update before 6 | trying it out for real. 7 | 8 | ## Amend the CloudFormation Template 9 | 10 | Edit the CloudFormation template originally used to create the stack, to make 11 | the necessary changes. It is also possible to run an update in which only the 12 | parameters passed to the template change. This module can accept the template as 13 | either a JSON string, an object, or a URL to a template uploaded to S3. Use the 14 | latter method for larger templates, as it has a larger maximum size limit. 15 | 16 | ## Run the Update 17 | 18 | Run the following code. 19 | 20 | ```js 21 | cloudFormationDeploy = require('cloudformation-deploy'); 22 | 23 | // Pull in the CloudFormation template from a JSON file or object. 24 | //var template = fs.readFileSync('example.json', { encoding: 'utf8' }); 25 | //var template = { ... }; 26 | // Or specify a URL. 27 | var template = 'http://s3.amazonaws.com/bucket/example.json'; 28 | 29 | var config = { 30 | // -------------------- 31 | // Required properties. 32 | // -------------------- 33 | 34 | // The name of the stack to be updated. 35 | stackName: 'example-stack-15', 36 | 37 | // -------------------- 38 | // Optional properties. 39 | // -------------------- 40 | 41 | // If defined, this property is passed to the AWS SDK client. It is not 42 | // recommended to use this approach, see above for comments on configuring 43 | // AWS access via the environment. 44 | // clientOptions: { 45 | // accessKeyId: 'akid', 46 | // secretAccessKey: 'secret', 47 | // region: 'us-east-1' 48 | // }, 49 | 50 | // Needed for stacks that affect permissions, which is most application stacks 51 | // these days. 52 | // See: http://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html 53 | capabilities: [ 54 | cloudFormationDeploy.capabilities.CAPABILITY_IAM, 55 | cloudFormationDeploy.capabilities.CAPABILITY_NAMED_IAM 56 | ], 57 | 58 | // Specify additional tags to apply to the stack. 59 | tags: { 60 | name: 'value' 61 | }, 62 | 63 | // Pass in any parameters required by the template. 64 | parameters: { 65 | name: 'value' 66 | }, 67 | 68 | // Seconds to wait between each check on the progress of stack creation or 69 | // deletion. 70 | progressCheckIntervalInSeconds: 10, 71 | 72 | // A function invoked whenever a CloudFormation event is created during 73 | // stack creation or deletion. 74 | onEventFn: function (event) { 75 | console.log(event); 76 | } 77 | }; 78 | 79 | cloudFormationDeploy.update(config, template, function (error, results) { 80 | if (error) { 81 | console.error(error); 82 | } 83 | 84 | // Whether or not there is an error, the results object is returned. It will 85 | // usually have additional useful information on why the stack update 86 | // failed. On success it will include the stack description, outputs 87 | // defined in the CloudFormation template, and events. 88 | console.log(results); 89 | }); 90 | ``` 91 | 92 | ## Update Configuration 93 | 94 | The `config` object passed to `cloudFormationDeploy.update` supports the 95 | following required and optional properties. 96 | 97 | ### Required Properties 98 | 99 | `stackName` - `string` - The name of the stack to update. 100 | 101 | ### Optional Properties 102 | 103 | `tags` - `object` - The tags to apply to the stack in addition to those created 104 | automatically based on the `baseName`. Tag values must be strings. 105 | 106 | `parameters` - `object` - Values to apply to the parameters in the 107 | CloudFormation template. Parameter values must be strings. 108 | 109 | `progressCheckIntervalInSeconds` - `number` - Number of seconds to wait between each 110 | check on the progress of stack creation or deletion. Defaults to `10`. 111 | 112 | `onEventFn` - `function` - A function invoked whenever a new event is 113 | created during stack creation or deletion. 114 | 115 | ``` 116 | function (event) { 117 | console.log(event); 118 | } 119 | ``` 120 | 121 | ## Failure Cases 122 | 123 | All failure cases will result in `cloudFormationDeploy.update` calling back with 124 | an error. The stack will attempt to roll back to its state prior to the update, 125 | which will either succeed or fail depending on the details of the update. In 126 | either case, the error message returned should identify the outcome and point to 127 | the root of the problem. 128 | 129 | [1]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html 130 | -------------------------------------------------------------------------------- /examples/ec2DeployFailure.js: -------------------------------------------------------------------------------- 1 | /* eslint no-extend-native: 0 */ 2 | /** 3 | * @fileOverview An example use of the module. 4 | * 5 | * This attempts to deploy a simple EC2 stack using one of the simple AWS 6 | * example templates. It will fail and delete the stack it was trying to create. 7 | * 8 | * To run: 9 | * 10 | * node examples/ec2DeployFailure.js 11 | * 12 | * Before running you must: 13 | * 14 | * 1) Set up suitable AWS credentials in the local environment beforehand. Read 15 | * the documentation for more on this topic. 16 | * 17 | * 2) Create an EC2 key pair called cloudformation-deploy-example, or change the 18 | * parameters.KeyName value to an existing key pair. 19 | */ 20 | 21 | var util = require('util'); 22 | var example = require('./lib/ec2DeployBase'); 23 | 24 | // The deployment fails because this instance type requires deployment in a VPC. 25 | example.run('t2.micro', function (error, result) { 26 | // This enables error messages to show up in the JSON output. Not something to 27 | // be used outside of example code. 28 | Object.defineProperty(Error.prototype, 'message', { 29 | configurable: true, 30 | enumerable: true 31 | }); 32 | 33 | // This will be a large set of data even for smaller deployments. 34 | console.log(util.format( 35 | 'Result: %s', 36 | JSON.stringify(result, null, ' ') 37 | )); 38 | 39 | if (error) { 40 | console.error(error.message, error.stack || ''); 41 | console.error('Deployment failed.'); 42 | } 43 | else { 44 | console.log('Deployment successful.'); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /examples/ec2DeploySuccess.js: -------------------------------------------------------------------------------- 1 | /* eslint no-extend-native: 0 */ 2 | /** 3 | * @fileOverview An example use of the module. 4 | * 5 | * This attempts to deploy a simple EC2 stack using one of the simple AWS 6 | * example templates. On success it will delete any prior instances deployed 7 | * via the same means. Run it twice to see this in action. 8 | * 9 | * To run: 10 | * 11 | * node examples/ec2DeploySuccess.js 12 | * 13 | * Before running you must: 14 | * 15 | * 1) Set up suitable AWS credentials in the local environment beforehand. Read 16 | * the documentation for more on this topic. 17 | * 18 | * 2) Create an EC2 key pair called cloudformation-deploy-example, or change the 19 | * parameters.KeyName value to an existing key pair. 20 | */ 21 | 22 | var util = require('util'); 23 | var example = require('./lib/ec2DeployBase'); 24 | 25 | example.run('t1.micro', function (error, result) { 26 | // This enables error messages to show up in the JSON output. Not something to 27 | // be used outside of example code. 28 | Object.defineProperty(Error.prototype, 'message', { 29 | configurable: true, 30 | enumerable: true 31 | }); 32 | 33 | // This will be a large set of data even for smaller deployments. 34 | console.log(util.format( 35 | 'Result: %s', 36 | JSON.stringify(result, null, ' ') 37 | )); 38 | 39 | if (error) { 40 | console.error(error.message, error.stack || ''); 41 | console.error('Deployment failed.'); 42 | } 43 | else { 44 | console.log('Deployment successful.'); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /examples/ec2PreviewUpdate.js: -------------------------------------------------------------------------------- 1 | /* eslint no-extend-native: 0 */ 2 | /** 3 | * @fileOverview An example use of the module. 4 | * 5 | * This previews what will be a failed update a simple EC2 stack using one of 6 | * the simple AWS example templates. 7 | * 8 | * To run: 9 | * 10 | * node examples/ec2DeployFailure.js 11 | * 12 | * Before running you must use ec2DeploySuccess.js to create a stack, and take 13 | * note of the stack name, to supply to this script. 14 | */ 15 | 16 | var util = require('util'); 17 | var example = require('./lib/ec2PreviewUpdateBase'); 18 | 19 | if (process.argv.length < 3) { 20 | console.error('You must provide a stackName argument.'); 21 | process.exit(1); 22 | } 23 | 24 | var stackName = process.argv[2]; 25 | 26 | example.run(stackName, function (error, result) { 27 | // This enables error messages to show up in the JSON output. Not something to 28 | // be used outside of example code. 29 | Object.defineProperty(Error.prototype, 'message', { 30 | configurable: true, 31 | enumerable: true 32 | }); 33 | 34 | // This will be a large set of data even for smaller deployments. 35 | console.log(util.format( 36 | 'Result: %s', 37 | JSON.stringify(result, null, ' ') 38 | )); 39 | 40 | if (error) { 41 | console.error(error.message, error.stack || ''); 42 | console.error('Preview of update failed.'); 43 | } 44 | else { 45 | console.log('Preview successful.'); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /examples/ec2UpdateFailure.js: -------------------------------------------------------------------------------- 1 | /* eslint no-extend-native: 0 */ 2 | /** 3 | * @fileOverview An example use of the module. 4 | * 5 | * This attempts to update a simple EC2 stack using one of the simple AWS 6 | * example templates. It will fail and roll back. 7 | * 8 | * To run: 9 | * 10 | * node examples/ec2DeployFailure.js 11 | * 12 | * Before running you must use ec2DeploySuccess.js to create a stack, and take 13 | * note of the stack name, to supply to this script. 14 | */ 15 | 16 | var util = require('util'); 17 | var example = require('./lib/ec2UpdateBase'); 18 | 19 | if (process.argv.length < 3) { 20 | console.error('You must provide a stackName argument.'); 21 | process.exit(1); 22 | } 23 | 24 | var stackName = process.argv[2]; 25 | 26 | // The deployment fails because this instance type requires deployment in a VPC. 27 | example.run(stackName, 't2.micro', function (error, result) { 28 | // This enables error messages to show up in the JSON output. Not something to 29 | // be used outside of example code. 30 | Object.defineProperty(Error.prototype, 'message', { 31 | configurable: true, 32 | enumerable: true 33 | }); 34 | 35 | // This will be a large set of data even for smaller deployments. 36 | console.log(util.format( 37 | 'Result: %s', 38 | JSON.stringify(result, null, ' ') 39 | )); 40 | 41 | if (error) { 42 | console.error(error.message, error.stack || ''); 43 | console.error('Update failed.'); 44 | } 45 | else { 46 | console.log('Update successful.'); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /examples/ec2UpdateSuccess.js: -------------------------------------------------------------------------------- 1 | /* eslint no-extend-native: 0 */ 2 | /** 3 | * @fileOverview An example use of the module. 4 | * 5 | * This attempts to update a simple EC2 stack using one of the simple AWS 6 | * example templates. 7 | * 8 | * To run: 9 | * 10 | * node examples/ec2DeployFailure.js 11 | * 12 | * Before running you must use ec2DeploySuccess.js to create a stack, and take 13 | * note of the stack name, to supply to this script. 14 | */ 15 | 16 | var util = require('util'); 17 | var example = require('./lib/ec2UpdateBase'); 18 | 19 | if (process.argv.length < 3) { 20 | console.error('You must provide a stackName argument.'); 21 | process.exit(1); 22 | } 23 | 24 | var stackName = process.argv[2]; 25 | 26 | example.run(stackName, 't1.micro', function (error, result) { 27 | // This enables error messages to show up in the JSON output. Not something to 28 | // be used outside of example code. 29 | Object.defineProperty(Error.prototype, 'message', { 30 | configurable: true, 31 | enumerable: true 32 | }); 33 | 34 | // This will be a large set of data even for smaller deployments. 35 | console.log(util.format( 36 | 'Result: %s', 37 | JSON.stringify(result, null, ' ') 38 | )); 39 | 40 | if (error) { 41 | console.error(error.message, error.stack || ''); 42 | console.error('Update failed.'); 43 | } 44 | else { 45 | console.log('Update successful.'); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /examples/lib/ec2DeployBase.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Supporting code for the examples. 3 | * 4 | * This deploys an EC2 stack from one of the example AWS templates. 5 | * 6 | * First create an EC2 key pair called 'cloudformation-deploy-example.' 7 | * 8 | * Depending on the instanceType specified it can be made to succeed or fail. 9 | * This is helpful when wanting to demonstrate behavior of the deployment code 10 | * on success or failure. 11 | * 12 | * Fail on: t2.micro. 13 | * Succeed on: t1.micro, m1.small. 14 | */ 15 | 16 | // Core. 17 | var fs = require('fs'); 18 | var path = require('path'); 19 | var util = require('util'); 20 | 21 | // Local. 22 | var cloudFormationDeploy = require('../../index'); 23 | 24 | /** 25 | * Run the deployment. 26 | * 27 | * @param {String} instanceType A valid EC2 instance type. 28 | * @param {Function} callback Of the form function (error, result). 29 | */ 30 | exports.run = function (instanceType, callback) { 31 | 32 | var unixTimestamp = Math.round((new Date()).getTime() / 1000); 33 | 34 | var config = { 35 | // If defined, this property is passed to the AWS SDK client. It is not 36 | // recommended to use this approach, but instead configure the client via 37 | // the environment. 38 | // clientOptions : { 39 | // accessKeyId: 'akid', 40 | // secretAccessKey: 'secret', 41 | // region: 'us-east-1' 42 | // }, 43 | baseName: 'ec2-cloudformation-deploy', 44 | version: '0.1.0', 45 | // This should usually be a build ID generated by a task manager, or other 46 | // unique number for this particular stack. Using a Unix timestamp is a fair 47 | // fallback for the sake of making this example run. 48 | deployId: unixTimestamp, 49 | progressCheckIntervalInSeconds: 3, 50 | // Parameters provided to the CloudFormation template. 51 | parameters: { 52 | // You must create an EC2 Key Pair with this name. 53 | KeyName: 'cloudformation-deploy-example', 54 | InstanceType: instanceType 55 | }, 56 | // Invoked once for each new event during stack creation and deletion. 57 | onEventFn: function (event) { 58 | console.log(util.format( 59 | 'Event: %s', 60 | JSON.stringify(event) 61 | )); 62 | }, 63 | // Invoked after stack creation is successful, but before any prior stacks 64 | // are deleted. Usually used to switch over resources to point to the new 65 | // stack, but here just an excuse for more example logging. 66 | postCreationFn: function (stackDescription, innerCallback) { 67 | console.log(util.format( 68 | 'Deployed stack description: %s', 69 | JSON.stringify(stackDescription, null, ' ') 70 | )); 71 | 72 | innerCallback(); 73 | } 74 | }; 75 | 76 | var templatePath = path.join(__dirname, '../templates/ec2.json'); 77 | var template = fs.readFileSync(templatePath, { 78 | encoding: 'utf8' 79 | }); 80 | 81 | cloudFormationDeploy.deploy(config, template, callback); 82 | }; 83 | -------------------------------------------------------------------------------- /examples/lib/ec2PreviewUpdateBase.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Supporting code for the examples. 3 | * 4 | * This previews an update of an EC2 stack from one of the example AWS 5 | * templates. 6 | * 7 | * First create a stack using the ec2DeploySuccess.js example, then preview an 8 | * update with this code. 9 | */ 10 | 11 | // Core. 12 | var fs = require('fs'); 13 | var path = require('path'); 14 | 15 | // Local. 16 | var cloudFormationDeploy = require('../../index'); 17 | 18 | /** 19 | * Run the deployment. 20 | * 21 | * @param {String} stackName The stack to update. 22 | * @param {Function} callback Of the form function (error, result). 23 | */ 24 | exports.run = function (stackName, callback) { 25 | var config = { 26 | // If defined, this property is passed to the AWS SDK client. It is not 27 | // recommended to use this approach, but instead configure the client via 28 | // the environment. 29 | // clientOptions : { 30 | // accessKeyId: 'akid', 31 | // secretAccessKey: 'secret', 32 | // region: 'us-east-1' 33 | // }, 34 | changeSetName: stackName + '-changeset', 35 | stackName: stackName, 36 | deleteChangeSet: true, 37 | progressCheckIntervalInSeconds: 3, 38 | // Parameters provided to the CloudFormation template. 39 | parameters: { 40 | // You must create an EC2 Key Pair with this name. 41 | KeyName: 'cloudformation-deploy-example', 42 | InstanceType: 'm1.small' 43 | } 44 | }; 45 | 46 | var templatePath = path.join(__dirname, '../templates/ec2.json'); 47 | var template = fs.readFileSync(templatePath, { 48 | encoding: 'utf8' 49 | }); 50 | 51 | cloudFormationDeploy.previewUpdate(config, template, callback); 52 | }; 53 | -------------------------------------------------------------------------------- /examples/lib/ec2UpdateBase.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Supporting code for the examples. 3 | * 4 | * This updates an EC2 stack from one of the example AWS templates. 5 | * 6 | * First create a stack using the ec2DeploySuccess.js example, then update it 7 | * with this code. 8 | * 9 | * Depending on the instanceType specified it can be made to succeed or fail. 10 | * This is helpful when wanting to demonstrate behavior of the deployment code 11 | * on success or failure. 12 | * 13 | * Fail on: t2.micro. 14 | * Succeed on: t1.micro, m1.small. 15 | */ 16 | 17 | // Core. 18 | var fs = require('fs'); 19 | var path = require('path'); 20 | var util = require('util'); 21 | 22 | // Local. 23 | var cloudFormationDeploy = require('../../index'); 24 | 25 | /** 26 | * Run the deployment. 27 | * 28 | * @param {String} stackName The stack to update. 29 | * @param {String} instanceType A valid EC2 instance type. 30 | * @param {Function} callback Of the form function (error, result). 31 | */ 32 | exports.run = function (stackName, instanceType, callback) { 33 | var config = { 34 | // If defined, this property is passed to the AWS SDK client. It is not 35 | // recommended to use this approach, but instead configure the client via 36 | // the environment. 37 | // clientOptions : { 38 | // accessKeyId: 'akid', 39 | // secretAccessKey: 'secret', 40 | // region: 'us-east-1' 41 | // }, 42 | stackName: stackName, 43 | progressCheckIntervalInSeconds: 3, 44 | // Parameters provided to the CloudFormation template. 45 | parameters: { 46 | // You must create an EC2 Key Pair with this name. 47 | KeyName: 'cloudformation-deploy-example', 48 | InstanceType: instanceType 49 | }, 50 | // Invoked once for each new event during stack creation and deletion. 51 | onEventFn: function (event) { 52 | console.log(util.format( 53 | 'Event: %s', 54 | JSON.stringify(event) 55 | )); 56 | } 57 | }; 58 | 59 | var templatePath = path.join(__dirname, '../templates/ec2.json'); 60 | var template = fs.readFileSync(templatePath, { 61 | encoding: 'utf8' 62 | }); 63 | 64 | cloudFormationDeploy.update(config, template, callback); 65 | }; 66 | -------------------------------------------------------------------------------- /examples/templates/ec2.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "AWS CloudFormation Sample Template EC2InstanceWithSecurityGroupSample: Create an Amazon EC2 instance running the Amazon Linux AMI. The AMI is chosen based on the region in which the stack is run. This example creates an EC2 security group for the instance to give you SSH access. **WARNING** This template creates an Amazon EC2 instance. You will be billed for the AWS resources used if you create a stack from this template.", 4 | "Parameters": { 5 | "KeyName": { 6 | "Description": "Name of and existing EC2 KeyPair to enable SSH access to the instance", 7 | "Type": "String" 8 | }, 9 | "InstanceType": { 10 | "Description": "WebServer EC2 instance type", 11 | "Type": "String", 12 | "Default": "m1.small", 13 | "AllowedValues": [ 14 | "t1.micro", 15 | "t2.micro", 16 | "t2.small", 17 | "t2.medium", 18 | "m1.small", 19 | "m1.medium", 20 | "m1.large", 21 | "m1.xlarge", 22 | "m2.xlarge", 23 | "m2.2xlarge", 24 | "m2.4xlarge", 25 | "m3.medium", 26 | "m3.large", 27 | "m3.xlarge", 28 | "m3.2xlarge", 29 | "c1.medium", 30 | "c1.xlarge", 31 | "c3.large", 32 | "c3.xlarge", 33 | "c3.2xlarge", 34 | "c3.4xlarge", 35 | "c3.8xlarge", 36 | "g2.2xlarge", 37 | "r3.large", 38 | "r3.xlarge", 39 | "r3.2xlarge", 40 | "r3.4xlarge", 41 | "r3.8xlarge", 42 | "i2.xlarge", 43 | "i2.2xlarge", 44 | "i2.4xlarge", 45 | "i2.8xlarge", 46 | "hi1.4xlarge", 47 | "hs1.8xlarge", 48 | "cr1.8xlarge", 49 | "cc2.8xlarge", 50 | "cg1.4xlarge" 51 | ], 52 | "ConstraintDescription": "must be a valid EC2 instance type." 53 | } 54 | }, 55 | "Mappings": { 56 | "RegionMap": { 57 | "us-east-1": { 58 | "AMI": "ami-7f418316" 59 | }, 60 | "us-west-1": { 61 | "AMI": "ami-951945d0" 62 | }, 63 | "us-west-2": { 64 | "AMI": "ami-16fd7026" 65 | }, 66 | "eu-west-1": { 67 | "AMI": "ami-24506250" 68 | }, 69 | "sa-east-1": { 70 | "AMI": "ami-3e3be423" 71 | }, 72 | "ap-southeast-1": { 73 | "AMI": "ami-74dda626" 74 | }, 75 | "ap-southeast-2": { 76 | "AMI": "ami-b3990e89" 77 | }, 78 | "ap-northeast-1": { 79 | "AMI": "ami-dcfa4edd" 80 | } 81 | } 82 | }, 83 | "Resources": { 84 | "Ec2Instance": { 85 | "Type": "AWS::EC2::Instance", 86 | "Properties": { 87 | "SecurityGroups": [ 88 | { 89 | "Ref": "InstanceSecurityGroup" 90 | } 91 | ], 92 | "KeyName": { 93 | "Ref": "KeyName" 94 | }, 95 | "ImageId": { 96 | "Fn::FindInMap": [ 97 | "RegionMap", 98 | { 99 | "Ref": "AWS::Region" 100 | }, 101 | "AMI" 102 | ] 103 | }, 104 | "InstanceType": { 105 | "Ref": "InstanceType" 106 | } 107 | } 108 | }, 109 | "InstanceSecurityGroup": { 110 | "Type": "AWS::EC2::SecurityGroup", 111 | "Properties": { 112 | "GroupDescription": "Enable SSH access via port 22", 113 | "SecurityGroupIngress": [] 114 | } 115 | } 116 | }, 117 | "Outputs": { 118 | "InstanceId": { 119 | "Description": "InstanceId of the newly created EC2 instance", 120 | "Value": { 121 | "Ref": "Ec2Instance" 122 | } 123 | }, 124 | "AZ": { 125 | "Description": "Availability Zone of the newly created EC2 instance", 126 | "Value": { 127 | "Fn::GetAtt": [ 128 | "Ec2Instance", 129 | "AvailabilityZone" 130 | ] 131 | } 132 | }, 133 | "PublicDNS": { 134 | "Description": "Public DNSName of the newly created EC2 instance", 135 | "Value": { 136 | "Fn::GetAtt": [ 137 | "Ec2Instance", 138 | "PublicDnsName" 139 | ] 140 | } 141 | }, 142 | "PublicIP": { 143 | "Description": "Public IP address of the newly created EC2 instance", 144 | "Value": { 145 | "Fn::GetAtt": [ 146 | "Ec2Instance", 147 | "PublicIp" 148 | ] 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Exposed interface for CloudFormation Deploy. 3 | */ 4 | 5 | // Local. 6 | var constants = require('./lib/constants'); 7 | var Deploy = require('./lib/deploy'); 8 | var PreviewUpdate = require('./lib/previewUpdate'); 9 | var Update = require('./lib/update'); 10 | 11 | // Exported constants. 12 | exports.capabilities = constants.capabilities; 13 | exports.onDeployFailure = constants.onDeployFailure; 14 | exports.priorInstance = constants.priorInstance; 15 | 16 | /** 17 | * Deploy the specified CloudFormation template to create a new stack or replace 18 | * an existing stack. 19 | * 20 | * See the documentation for the form of the config object. 21 | * 22 | * @param {Object} config Configuration. 23 | * @param {Object|String} template The CloudFormation template as either an 24 | * object or JSON string, or a URL to a template file in S3 in the same region 25 | * as the stack will be deployed to. 26 | * @param {Function} callback Of the form function (error, result). 27 | */ 28 | exports.deploy = function (config, template, callback) { 29 | var deploy = new Deploy(config, template); 30 | deploy.deploy(callback); 31 | }; 32 | 33 | /** 34 | * Preview a stack update by creating a changeset and inspecting its details. 35 | * 36 | * See the documentation for the form of the config object. 37 | * 38 | * @param {Object} config Configuration. 39 | * @param {Object|String} template The CloudFormation template as either an 40 | * object or JSON string, or a URL to a template file in S3 in the same region 41 | * as the stack will be deployed to. 42 | * @param {Function} callback Of the form function (error, result). 43 | */ 44 | exports.previewUpdate = function (config, template, callback) { 45 | var previewUpdate = new PreviewUpdate(config, template); 46 | previewUpdate.previewUpdate(callback); 47 | }; 48 | 49 | /** 50 | * Deploy the specified CloudFormation template to update an existing stack. 51 | * 52 | * See the documentation for the form of the config object. 53 | * 54 | * @param {Object} config Configuration. 55 | * @param {Object|String} template The CloudFormation template as either an 56 | * object or JSON string, or a URL to a template file in S3 in the same region 57 | * as the stack will be deployed to. 58 | * @param {Function} callback Of the form function (error, result). 59 | */ 60 | exports.update = function (config, template, callback) { 61 | var update = new Update(config, template); 62 | update.update(callback); 63 | }; 64 | -------------------------------------------------------------------------------- /lib/cloudFormation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview CloudFormation utility class definition. 3 | */ 4 | 5 | // Core. 6 | var util = require('util'); 7 | 8 | // NPM. 9 | var async = require('async'); 10 | var AWS = require('aws-sdk'); 11 | var _ = require('lodash'); 12 | 13 | // Local. 14 | var constants = require('./constants'); 15 | var utilities = require('./utilities'); 16 | 17 | // -------------------------------------------------------------------------- 18 | // Class definition. 19 | // -------------------------------------------------------------------------- 20 | 21 | /** 22 | * @class CloudFormation utility class. 23 | * 24 | * @param {Object} config Configuration object. 25 | */ 26 | function CloudFormation (config) { 27 | this.config = config; 28 | 29 | // The AWS.CloudFormation client will be set here. It is exported for test 30 | // purposes. 31 | // 32 | // Creation of the client is late and lazy because this helps with situations in 33 | // which you want to carry out different AWS actions with different 34 | // configurations in the same process. You can load this module up front and it 35 | // won't create the client until it is used. 36 | if (typeof this.config.clientOptions === 'object') { 37 | // Settings via config. Not recommended. 38 | this.client = new AWS.CloudFormation(this.config.clientOptions); 39 | } 40 | else { 41 | // Assuming the setting of credentials via environment variable, 42 | // credentials file, role, etc. 43 | this.client = new AWS.CloudFormation(); 44 | } 45 | 46 | // Status filters for listStacks API. 47 | this.stackStatusFilter = [ 48 | 'CREATE_IN_PROGRESS', 49 | 'CREATE_FAILED', 50 | 'CREATE_COMPLETE', 51 | 'ROLLBACK_IN_PROGRESS', 52 | 'ROLLBACK_FAILED', 53 | 'ROLLBACK_COMPLETE', 54 | 'DELETE_FAILED' 55 | ]; 56 | } 57 | 58 | // -------------------------------------------------------------------------- 59 | // Methods. 60 | // -------------------------------------------------------------------------- 61 | 62 | /** 63 | * Issue a request to start creating a changeset for a proposed stack update. 64 | * 65 | * Data returned has the form: 66 | * 67 | * { 68 | * // The changeset ID. 69 | * Id: '', 70 | * StackId: '' 71 | * } 72 | * 73 | * @param {String} template The template, or a URL to the template. 74 | * @param {Function} callback Of the form function (error, data). 75 | */ 76 | CloudFormation.prototype.createChangeSet = function (template, callback) { 77 | var params = { 78 | ChangeSetName: this.config.changeSetName, 79 | ChangeSetType: 'UPDATE', 80 | StackName: utilities.determineStackName(this.config), 81 | // Most stacks will need this, so may as well include it for all. 82 | Capabilities: this.config.capabilities, 83 | Parameters: utilities.getParameters(this.config), 84 | Tags: utilities.getTags(this.config) 85 | }; 86 | 87 | utilities.addTemplatePropertyToParameters(params, template); 88 | this.client.createChangeSet(params, function (error, data) { 89 | if (error) { 90 | return callback(new Error(util.format( 91 | 'Call to createChangeSet failed: %s', 92 | error 93 | ))); 94 | } 95 | 96 | callback(null, data); 97 | }); 98 | }; 99 | 100 | /** 101 | * Delete the changeset created for this given configuration. 102 | * 103 | * @param {String} changeSetName The changeset name. 104 | * @param {Function} callback Of the form function (error). 105 | */ 106 | CloudFormation.prototype.deleteChangeSet = function (callback) { 107 | var params = { 108 | ChangeSetName: this.config.changeSetName, 109 | StackName: utilities.determineStackName(this.config) 110 | }; 111 | 112 | this.client.deleteChangeSet(params, function (error) { 113 | if (error) { 114 | return callback(new Error(util.format( 115 | 'Call to deleteChangeSet failed: %s', 116 | error 117 | ))); 118 | } 119 | 120 | callback(); 121 | }); 122 | }; 123 | 124 | /** 125 | * Describe a changeset in order to obtain the necessary information on the 126 | * changes the update will produce. 127 | * 128 | * The data is as follows: 129 | * 130 | * { 131 | * ChangeSetName: '', 132 | * ChangeSetId: '', 133 | * StackName: '', 134 | * Description: '', 135 | * Parameters: [ 136 | * { 137 | * ParameterKey: '', 138 | * ParameterValue: '', 139 | * UsePreviousParameterValue: true 140 | * }, 141 | * ... 142 | * ], 143 | * CreationTime: Date, 144 | * ExecutionStatus: '', 145 | * Status: '', 146 | * StatusReason: '', 147 | * Changes: [ 148 | * { 149 | * Type: 'Resource', 150 | * ResourceChange: { 151 | * Action: '', 152 | * LogicalResourceId: '', 153 | * PhysicalResourceId: '', 154 | * ResourceType: '', 155 | * Replacement: '', 156 | * Scope: ['', ...], 157 | * Details: [ 158 | * { 159 | * Target: { 160 | * Attribute: '', 161 | * Name: '', 162 | * RequiresRecreation: '' 163 | * }, 164 | * Evaluation: '', 165 | * ChangeSource: '', 166 | * CausingEntity: '' 167 | * }, 168 | * ... 169 | * ] 170 | * } 171 | * }, 172 | * ... 173 | * ], 174 | * // Present for large responses that page across the list of changes. 175 | * NextToken: '' 176 | * } 177 | * 178 | * @param {String} changeSetName The changeset name. 179 | * @param {Function} callback Of the form function (error, data). 180 | */ 181 | CloudFormation.prototype.describeChangeSet = function (callback) { 182 | var self = this; 183 | var data; 184 | 185 | /** 186 | * Recursively load pages for the describeChangeSet response. 187 | * 188 | * @param {String} [nextToken] The token to obtain the next page. 189 | */ 190 | function loadPage (nextToken) { 191 | var params = { 192 | ChangeSetName: self.config.changeSetName, 193 | StackName: utilities.determineStackName(self.config) 194 | }; 195 | 196 | if (nextToken) { 197 | params.NextToken = nextToken; 198 | } 199 | 200 | self.client.describeChangeSet(params, function (error, dataPage) { 201 | if (error) { 202 | return callback(new Error(util.format( 203 | 'Call to describeChangeSet failed: %s', 204 | error 205 | ))); 206 | } 207 | 208 | // The assumption here is that only the Changes property is getting paged, 209 | // and that tags and parameters will not be effectively paged - all should 210 | // be in the first page. 211 | if (data) { 212 | data.Changes = data.Changes.concat(dataPage.Changes); 213 | } 214 | else { 215 | data = _.omit(dataPage, ['NextToken']); 216 | } 217 | 218 | if (dataPage.NextToken) { 219 | loadPage(dataPage.NextToken); 220 | } 221 | else { 222 | callback(null, data); 223 | } 224 | }); 225 | } 226 | 227 | loadPage(); 228 | }; 229 | 230 | /** 231 | * Issue a request to start creation of a stack. 232 | * 233 | * Data returned has the form: 234 | * 235 | * { 236 | * StackId: '' 237 | * } 238 | * 239 | * @param {String} template The template, or a URL to the template. 240 | * @param {Function} callback Of the form function (error, data). 241 | */ 242 | CloudFormation.prototype.createStack = function (template, callback) { 243 | var params = { 244 | StackName: utilities.determineStackName(this.config), 245 | // Most stacks will need this, so may as well include it for all. 246 | Capabilities: this.config.capabilities, 247 | OnFailure: this.config.onDeployFailure, 248 | Parameters: utilities.getParameters(this.config), 249 | Tags: utilities.getTags(this.config), 250 | TimeoutInMinutes: this.config.createStackTimeoutInMinutes 251 | }; 252 | 253 | utilities.addTemplatePropertyToParameters(params, template); 254 | this.client.createStack(params, function (error, data) { 255 | if (error) { 256 | return callback(new Error(util.format( 257 | 'Call to createStack failed: %s', 258 | error 259 | ))); 260 | } 261 | 262 | callback(null, data); 263 | }); 264 | }; 265 | 266 | /** 267 | * Issue a request to start updating a stack. 268 | * 269 | * Data returned has the form: 270 | * 271 | * { 272 | * StackId: '' 273 | * } 274 | * 275 | * @param {String} template The template, or a URL to the template. 276 | * @param {Function} callback Of the form function (error, data). 277 | */ 278 | CloudFormation.prototype.updateStack = function (template, callback) { 279 | var params = { 280 | StackName: utilities.determineStackName(this.config), 281 | // Most stacks will need this, so may as well include it for all. 282 | Capabilities: this.config.capabilities, 283 | Parameters: utilities.getParameters(this.config), 284 | Tags: utilities.getTags(this.config) 285 | }; 286 | 287 | utilities.addTemplatePropertyToParameters(params, template); 288 | this.client.updateStack(params, function (error, data) { 289 | if (error) { 290 | return callback(new Error(util.format( 291 | 'Call to updateStack failed: %s', 292 | error 293 | ))); 294 | } 295 | 296 | callback(null, data); 297 | }); 298 | }; 299 | 300 | /** 301 | * Issue a request to start deletion of a stack. 302 | * 303 | * @param {String} stackId The stack ID. 304 | * @param {Function} callback Of the form function (error). 305 | */ 306 | CloudFormation.prototype.deleteStack = function (stackId, callback) { 307 | var params = { 308 | StackName: stackId 309 | }; 310 | 311 | this.client.deleteStack(params, function (error) { 312 | if (error) { 313 | return callback(new Error(util.format( 314 | 'Call to deleteStack failed: %s', 315 | error 316 | ))); 317 | } 318 | 319 | callback(); 320 | }); 321 | }; 322 | 323 | /** 324 | * Obtain a stack description, which will include the parameters specified in 325 | * the Outputs section of the CloudFormation template. 326 | * 327 | * @param {String} stackId The stack ID. 328 | * @param {Function} callback Of the form function (error). 329 | */ 330 | CloudFormation.prototype.describeStack = function (stackId, callback) { 331 | var params = { 332 | StackName: stackId 333 | }; 334 | 335 | this.client.describeStacks(params, function (error, result) { 336 | if (error) { 337 | return callback(new Error(util.format( 338 | 'Call to describeStacks failed: %s', 339 | error 340 | ))); 341 | } 342 | 343 | if (!result.Stacks.length) { 344 | return callback(new Error(util.format( 345 | 'No such stack: %s', 346 | stackId 347 | ))); 348 | } 349 | 350 | callback(null, result.Stacks[0]); 351 | }); 352 | }; 353 | 354 | /** 355 | * Obtain a stack description for all of the stacks with tags matching the newly 356 | * deploy stack's base name tag. 357 | * 358 | * This unfortunately requires listing all stacks in the account, but that's 359 | * actually not too terrible even in an account with thousands of stacks. 360 | * 361 | * @param {String} stackBaseName The base name of the stack, for matching. 362 | * @param {String} createdStackId The currently created stack, not to be 363 | * included in this list. 364 | * @param {Function} callback Of the form function (error). 365 | */ 366 | CloudFormation.prototype.describePriorStacks = function (stackBaseName, createdStackId, callback) { 367 | var self = this; 368 | var params = { 369 | // Filter down to status that indicates a running stack not involved in some 370 | // form of update or delete. 371 | StackStatusFilter: this.stackStatusFilter 372 | }; 373 | var stackSummaries = []; 374 | var stackDescriptions = []; 375 | 376 | function recurse () { 377 | self.client.listStacks(params, function (error, result) { 378 | if (error) { 379 | return callback(new Error(util.format( 380 | 'Call to describeStacks failed: %s', 381 | error 382 | ))); 383 | } 384 | 385 | // Filter down to only those stacks with similar stack names, where the 386 | // baseName component is the same. 387 | stackSummaries = stackSummaries.concat(_.filter( 388 | result.StackSummaries, 389 | function (stackSummary) { 390 | // Match on the name, but exclude the freshly created stack. 391 | return utilities.baseNameMatchesStackName( 392 | stackBaseName, 393 | stackSummary.StackName 394 | ) && stackSummary.StackId !== createdStackId; 395 | } 396 | )); 397 | 398 | // If there are too many stack summaries to get at one go, then fetch 399 | // the next page. 400 | if (result.NextToken) { 401 | params.NextToken = result.NextToken; 402 | return recurse(); 403 | } 404 | 405 | if (!stackSummaries.length) { 406 | return callback(null, stackDescriptions); 407 | } 408 | 409 | // Now fetch a description for each of the near-match summaries. This is 410 | // done in series just in case there are a hundred of them. The standard 411 | // situation is that there will be one, or at most two if there have been 412 | // deployment failures. 413 | // 414 | // Check the tags to make sure that each is actually a prior stack. 415 | async.eachSeries(stackSummaries, function (stackSummary, asyncCallback) { 416 | self.describeStack( 417 | stackSummary.StackId, 418 | function (describeStackError, stackDescription) { 419 | if (describeStackError) { 420 | return asyncCallback(describeStackError); 421 | } 422 | 423 | var isPriorStack = _.some(stackDescription.Tags, function (tag) { 424 | // Depends on the order of properties, but that should be ok. 425 | return JSON.stringify(tag) === JSON.stringify({ 426 | Key: constants.tag.STACK_BASE_NAME, 427 | Value: stackBaseName 428 | }); 429 | }); 430 | 431 | if (isPriorStack) { 432 | stackDescriptions.push(stackDescription); 433 | } 434 | 435 | asyncCallback(); 436 | } 437 | ); 438 | }, function (asyncError) { 439 | callback(asyncError, stackDescriptions); 440 | }); 441 | }); 442 | } 443 | 444 | recurse(); 445 | }; 446 | 447 | /** 448 | * Request information on a stack. 449 | * 450 | * Returns events in chronological order. 451 | * 452 | * @param {String} stackId The stack ID. 453 | * @param {Function} callback Of the form function (error, object[]). 454 | */ 455 | CloudFormation.prototype.describeStackEvents = function (stackId, callback) { 456 | var self = this; 457 | var params = { 458 | StackName: stackId 459 | }; 460 | var events = []; 461 | 462 | function recurse () { 463 | self.client.describeStackEvents(params, function (error, result) { 464 | if (error) { 465 | return callback(new Error(util.format( 466 | 'Call to describeStackEvents failed: %s', 467 | error 468 | ))); 469 | } 470 | 471 | events = events.concat(result.StackEvents); 472 | 473 | // This shouldn't happen for most stacks, but if there are too many 474 | // events to get at one go, then fetch the next page. 475 | if (result.NextToken) { 476 | params.NextToken = result.NextToken; 477 | return recurse(); 478 | } 479 | 480 | // Events arrive in reverse chronological order, but that's inconvenient 481 | // for the sort of processing we want to carry out, so reverse it. 482 | callback(null, events.reverse()); 483 | }); 484 | } 485 | 486 | recurse(); 487 | }; 488 | 489 | /** 490 | * Validate a CloudFormation template. 491 | * 492 | * @param {Object|String} template Either an object, JSON, or a URL. 493 | * @param {Function} callback Of the form function (error). 494 | */ 495 | CloudFormation.prototype.validateTemplate = function (template, callback) { 496 | var params = {}; 497 | 498 | utilities.addTemplatePropertyToParameters(params, template); 499 | this.client.validateTemplate(params, function (error) { 500 | if (error) { 501 | return callback(new Error(util.format( 502 | 'Call to validateTemplate failed: %s', 503 | error 504 | ))); 505 | } 506 | 507 | callback(); 508 | }); 509 | }; 510 | 511 | // -------------------------------------------------------------------------- 512 | // Exports constructor. 513 | // -------------------------------------------------------------------------- 514 | 515 | module.exports = CloudFormation; 516 | -------------------------------------------------------------------------------- /lib/cloudFormationOperation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview A parent class definition for CloudFormation operations. 3 | */ 4 | 5 | // Core. 6 | var util = require('util'); 7 | 8 | // NPM. 9 | var async = require('async'); 10 | var _ = require('lodash'); 11 | 12 | // Local. 13 | var CloudFormation = require('./cloudFormation'); 14 | var constants = require('./constants'); 15 | 16 | // -------------------------------------------------------------------------- 17 | // Class definition. 18 | // -------------------------------------------------------------------------- 19 | 20 | /** 21 | * @class Deployment interface class. 22 | * 23 | * @param {Object} config Configuration object. 24 | * @param {Object|String} template The CloudFormation template as either an 25 | * object or JSON string, or a URL to a template file in S3 in the same region 26 | * as the stack will be deployed to. 27 | */ 28 | function CloudFormationOperation (config, template) { 29 | this.config = config; 30 | this.template = template; 31 | this.cloudFormation = new CloudFormation(this.config); 32 | } 33 | 34 | // ------------------------------------------------------------------------- 35 | // Methods. 36 | // ------------------------------------------------------------------------- 37 | 38 | /** 39 | * Get a data object to be contained by the overall result object. 40 | * 41 | * @param {String} stackName The full name of the stack. 42 | * @param {String} stackId The unique ID of the stack, if known. 43 | * @return {Object} An object to be contained in the larger result object. 44 | */ 45 | CloudFormationOperation.prototype.getStackData = function (stackName, stackId) { 46 | return { 47 | stackName: stackName, 48 | stackId: stackId, 49 | status: undefined, 50 | events: [] 51 | }; 52 | }; 53 | 54 | /** 55 | * Load events for the stack and update the stackData. 56 | * 57 | * @param {Object} stackData The unique name of the stack to wait on. 58 | * @param {Function} callback Of the form function (error). 59 | */ 60 | CloudFormationOperation.prototype.updateEventData = function (stackData, callback) { 61 | var self = this; 62 | // This provides us with all events, annoyingly. Can't constrain it to just 63 | // the recent ones we're interested in. 64 | this.cloudFormation.describeStackEvents( 65 | stackData.stackId, 66 | function (error, events) { 67 | if (error) { 68 | return callback(error); 69 | } 70 | 71 | // Just look at the new events from this time around. 72 | var newEvents = events.slice(stackData.events.length); 73 | // Replace existing events data with what we get. 74 | stackData.events = events; 75 | 76 | // Fire off functions in response to events. 77 | if (typeof self.config.onEventFn === 'function') { 78 | _.each(newEvents, self.config.onEventFn); 79 | } 80 | 81 | // Events are in chronological order. So look for the last event that refers 82 | // to the template itself and the current status is that status. 83 | newEvents = _.dropRightWhile(newEvents, function (event) { 84 | return event.ResourceType !== constants.resourceType.STACK; 85 | }); 86 | 87 | if (newEvents.length) { 88 | stackData.status = _.last(newEvents).ResourceStatus; 89 | } 90 | 91 | callback(); 92 | } 93 | ); 94 | }; 95 | 96 | /** 97 | * Wait on the completion of a stack operation, such as creation, deletion, or 98 | * update. 99 | * 100 | * A stack creation that is set to automatically delete on failure will run 101 | * through all of a failed creation and then a successful deletion within this 102 | * one method. 103 | * 104 | * This adds events to the objects provided, and calls back with an error on 105 | * either a timeout or failure of the stack operation to complete. 106 | * 107 | * (Exported only to make testing easier, not because it should be used 108 | * directly). 109 | * 110 | * @param {Object} type The type of stack operation. 111 | * @param {Object} stackData The unique name of the stack to wait on. 112 | * @param {Function} callback Of the form function (error). 113 | */ 114 | CloudFormationOperation.prototype.awaitCompletion = function (type, stackData, callback) { 115 | var self = this; 116 | 117 | callback = _.once(callback); 118 | 119 | /** 120 | * Which status to watch for completion depends on whether or not 121 | * configuration is set to delete a failed stack automatically. 122 | * 123 | * @return {Boolean} True if complete. 124 | */ 125 | function isComplete () { 126 | if (type === constants.type.CREATE_STACK) { 127 | // If we are deleting a stack automatically on failed creation, then this 128 | // is only complete when we hit a completion for creation or deletion. In 129 | // this case CREATE_FAILED is just a step along the way. 130 | if (self.config.onDeployFailure === constants.onDeployFailure.DELETE) { 131 | return _.includes([ 132 | constants.resourceStatus.CREATE_COMPLETE, 133 | constants.resourceStatus.DELETE_COMPLETE, 134 | constants.resourceStatus.DELETE_FAILED 135 | ], stackData.status); 136 | } 137 | // Otherwise creation complete or failed status is good enough to stop on. 138 | else { 139 | return _.includes([ 140 | constants.resourceStatus.CREATE_COMPLETE, 141 | constants.resourceStatus.CREATE_FAILED 142 | ], stackData.status); 143 | } 144 | } 145 | // Otherwise for deleting a stack, check for the delete outcomes. 146 | else if (type === constants.type.DELETE_STACK) { 147 | return _.includes([ 148 | constants.resourceStatus.DELETE_COMPLETE, 149 | constants.resourceStatus.DELETE_FAILED 150 | ], stackData.status); 151 | } 152 | // Otherwise we are updating a stack, and looking for completion or rollback 153 | // outcomes. 154 | else { 155 | return _.includes([ 156 | constants.resourceStatus.UPDATE_COMPLETE, 157 | constants.resourceStatus.UPDATE_ROLLBACK_COMPLETE, 158 | constants.resourceStatus.UPDATE_ROLLBACK_FAILED 159 | ], stackData.status); 160 | } 161 | } 162 | 163 | async.until( 164 | // Truth test - continue running the next function argument until this test 165 | // returns true. 166 | isComplete, 167 | 168 | // Wait for the progress check interval then load the events and see what's 169 | // new. Update the stackData object along the way. 170 | function (asyncCallback) { 171 | setTimeout(function () { 172 | self.updateEventData(stackData, asyncCallback); 173 | }, self.config.progressCheckIntervalInSeconds * 1000); 174 | }, 175 | 176 | // Once the truth test returns true, or an error is generated, then here we 177 | // are. 178 | function (error) { 179 | if (error) { 180 | return callback(error); 181 | } 182 | 183 | // Look at the last status to figure out whether or not to call back with 184 | // an error. 185 | // 186 | // Note that we call back with an error on a successful delete of a failed 187 | // stack create. 188 | if ( 189 | type === constants.type.CREATE_STACK && 190 | stackData.status === constants.resourceStatus.CREATE_COMPLETE 191 | ) { 192 | callback(); 193 | } 194 | else if ( 195 | type === constants.type.DELETE_STACK && 196 | stackData.status === constants.resourceStatus.DELETE_COMPLETE 197 | ) { 198 | callback(); 199 | } 200 | else if ( 201 | type === constants.type.UPDATE_STACK && 202 | stackData.status === constants.resourceStatus.UPDATE_COMPLETE 203 | ) { 204 | callback(); 205 | } 206 | else { 207 | // Find the first interesting event likely to signal a failure. 208 | var failureEvent = _.find(stackData.events, function (event) { 209 | return event.ResourceStatus && /FAILED/.test(event.ResourceStatus); 210 | }); 211 | 212 | if (failureEvent) { 213 | callback(new Error(util.format( 214 | 'Stack operation failed on the following event: %s', 215 | JSON.stringify(failureEvent) 216 | ))); 217 | } 218 | else { 219 | callback(new Error(util.format( 220 | 'Stack operation failed, but could not identify failure event.' 221 | ))); 222 | } 223 | } 224 | } 225 | ); 226 | }; 227 | 228 | // -------------------------------------------------------------------------- 229 | // Exports constructor. 230 | // -------------------------------------------------------------------------- 231 | 232 | module.exports = CloudFormationOperation; 233 | -------------------------------------------------------------------------------- /lib/configValidator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview A configuration validator. 3 | */ 4 | 5 | // NPM. 6 | var jsonschema = require('jsonschema'); 7 | var _ = require('lodash'); 8 | 9 | // Local. 10 | var constants = require('./constants'); 11 | 12 | // -------------------------------------------------------------------------- 13 | // Schema definitions. 14 | // -------------------------------------------------------------------------- 15 | 16 | // For all operations. 17 | var sharedConfigSchema = { 18 | type: 'object', 19 | additionalProperties: false, 20 | properties: { 21 | 22 | // ---------------------------------------------------------------------- 23 | // Optional for the user, but we require them internally; they are set 24 | // as defaults prior to the check on validity. 25 | // ---------------------------------------------------------------------- 26 | 27 | capabilities: { 28 | type: 'array', 29 | items: { 30 | enum: _.values(constants.capabilities) 31 | }, 32 | required: true 33 | }, 34 | 35 | parameters: { 36 | type: 'object', 37 | patternProperties: { 38 | '.*': { 39 | type: 'string', 40 | required: false 41 | } 42 | }, 43 | required: true 44 | }, 45 | 46 | tags: { 47 | type: 'object', 48 | patternProperties: { 49 | '.*': { 50 | type: 'string', 51 | required: false 52 | } 53 | }, 54 | required: true 55 | }, 56 | 57 | // ---------------------------------------------------------------------- 58 | // Actually optional. 59 | // ---------------------------------------------------------------------- 60 | 61 | clientOptions: { 62 | type: 'object', 63 | required: false 64 | } 65 | 66 | }, 67 | required: true 68 | }; 69 | 70 | // For deployments. 71 | var deployConfigSchema = _.merge({}, sharedConfigSchema, { 72 | id: '/DeployConfig', 73 | additionalProperties: false, 74 | properties: { 75 | 76 | // ---------------------------------------------------------------------- 77 | // Required. 78 | // ---------------------------------------------------------------------- 79 | 80 | baseName: { 81 | type: 'string', 82 | minLength: 1, 83 | required: true 84 | }, 85 | 86 | version: { 87 | type: 'string', 88 | minLength: 1, 89 | required: true 90 | }, 91 | 92 | deployId: { 93 | anyOf: [ 94 | { 95 | type: 'string', 96 | minLength: 1 97 | }, 98 | { 99 | type: 'number' 100 | } 101 | ], 102 | required: true 103 | }, 104 | 105 | // ---------------------------------------------------------------------- 106 | // Optional for the user, but we require them internally; they are set 107 | // as defaults prior to the check on validity. 108 | // ---------------------------------------------------------------------- 109 | 110 | onDeployFailure: { 111 | enum: _.values(constants.onDeployFailure), 112 | required: true 113 | }, 114 | 115 | onEventFn: { 116 | isFunction: true, 117 | required: true 118 | }, 119 | 120 | postCreationFn: { 121 | isFunction: true, 122 | required: true 123 | }, 124 | 125 | priorInstance: { 126 | enum: _.values(constants.priorInstance), 127 | required: true 128 | }, 129 | 130 | progressCheckIntervalInSeconds: { 131 | type: 'number', 132 | minimum: 1, 133 | required: true 134 | }, 135 | 136 | createStackTimeoutInMinutes: { 137 | type: 'number', 138 | minimum: 0, 139 | required: true 140 | } 141 | } 142 | }); 143 | 144 | // For preview of an update via a changeset. 145 | var previewUpdateConfigSchema = _.merge({}, sharedConfigSchema, { 146 | id: '/previewUpdateConfig', 147 | additionalProperties: false, 148 | properties: { 149 | 150 | // ---------------------------------------------------------------------- 151 | // Required. 152 | // ---------------------------------------------------------------------- 153 | 154 | changeSetName: { 155 | type: 'string', 156 | minLength: 1, 157 | required: true 158 | }, 159 | 160 | stackName: { 161 | type: 'string', 162 | minLength: 1, 163 | required: true 164 | }, 165 | 166 | // ---------------------------------------------------------------------- 167 | // Optional for the user, but we require them internally; they are set 168 | // as defaults prior to the check on validity. 169 | // ---------------------------------------------------------------------- 170 | 171 | deleteChangeSet: { 172 | type: 'boolean', 173 | required: true 174 | }, 175 | 176 | progressCheckIntervalInSeconds: { 177 | type: 'number', 178 | minimum: 1, 179 | required: true 180 | } 181 | } 182 | }); 183 | 184 | // For stack updates. 185 | var updateConfigSchema = _.merge({}, sharedConfigSchema, { 186 | id: '/UpdateConfig', 187 | additionalProperties: false, 188 | properties: { 189 | 190 | // ---------------------------------------------------------------------- 191 | // Required. 192 | // ---------------------------------------------------------------------- 193 | 194 | stackName: { 195 | type: 'string', 196 | minLength: 1, 197 | required: true 198 | }, 199 | 200 | // ---------------------------------------------------------------------- 201 | // Optional for the user, but we require them internally; they are set 202 | // as defaults prior to the check on validity. 203 | // ---------------------------------------------------------------------- 204 | 205 | onEventFn: { 206 | isFunction: true, 207 | required: true 208 | }, 209 | 210 | progressCheckIntervalInSeconds: { 211 | type: 'number', 212 | minimum: 1, 213 | required: true 214 | } 215 | } 216 | }); 217 | 218 | // -------------------------------------------------------------------------- 219 | // Set up the validator. 220 | // -------------------------------------------------------------------------- 221 | 222 | var validator = new jsonschema.Validator(); 223 | 224 | /** 225 | * Since jsonschema doesn't seem to test function types properly at this point 226 | * in time, hack in an additional test. 227 | */ 228 | validator.attributes.isFunction = function (instance, schema, options, ctx) { 229 | var result = new jsonschema.ValidatorResult(instance, schema, options, ctx); 230 | 231 | if (!_.isBoolean(schema.isFunction)) { 232 | return result; 233 | } 234 | 235 | if (schema.isFunction) { 236 | if ((instance !== undefined) && (typeof instance !== 'function')) { 237 | result.addError('Required to be a function.'); 238 | } 239 | } 240 | else { 241 | if (typeof instance === 'function') { 242 | result.addError('Required to not be a function.'); 243 | } 244 | } 245 | 246 | return result; 247 | }; 248 | 249 | // -------------------------------------------------------------------------- 250 | // Exported functions. 251 | // -------------------------------------------------------------------------- 252 | 253 | /** 254 | * Validate the provided configuration against the provided schema. 255 | * 256 | * @param {Object} config Configuration. 257 | * @return {Error[]} An array of errors. 258 | */ 259 | exports.validate = function (config, schema) { 260 | var result = validator.validate(config, schema) || {}; 261 | return result.errors || []; 262 | }; 263 | 264 | /** 265 | * Validate the provided deploy configuration. 266 | * 267 | * @param {Object} config Deploy configuration. 268 | * @return {Error[]} An array of errors. 269 | */ 270 | exports.validateDeployConfig = function (config) { 271 | return exports.validate(config, deployConfigSchema); 272 | }; 273 | 274 | /** 275 | * Validate the provided preview update configuration. 276 | * 277 | * @param {Object} config Update configuration. 278 | * @return {Error[]} An array of errors. 279 | */ 280 | exports.validatePreviewUpdateConfig = function (config) { 281 | return exports.validate(config, previewUpdateConfigSchema); 282 | }; 283 | 284 | /** 285 | * Validate the provided update configuration. 286 | * 287 | * @param {Object} config Update configuration. 288 | * @return {Error[]} An array of errors. 289 | */ 290 | exports.validateUpdateConfig = function (config) { 291 | return exports.validate(config, updateConfigSchema); 292 | }; 293 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Various useful constants. 3 | */ 4 | 5 | // The available AWS stack capabilities values. 6 | exports.capabilities = { 7 | CAPABILITY_IAM: 'CAPABILITY_IAM', 8 | CAPABILITY_NAMED_IAM: 'CAPABILITY_NAMED_IAM' 9 | }; 10 | 11 | // Describing what to do with the current stack on failure to deploy. 12 | exports.onDeployFailure = { 13 | // Delete the stack. 14 | DELETE: 'DELETE', 15 | // Do nothing, leave the failed partial stack for diagnosis. Useful when 16 | // developing. 17 | DO_NOTHING: 'DO_NOTHING' 18 | }; 19 | 20 | // Describing what to do with prior instances of the stack being deployed, 21 | // following a successful deployment. 22 | exports.priorInstance = { 23 | // Delete the prior instances. 24 | DELETE: 'DELETE', 25 | // Do nothing, leave prior instances running. 26 | DO_NOTHING: 'DO_NOTHING' 27 | }; 28 | 29 | // By no means a full list, just the ones we care about. 30 | exports.changeSetStatus = { 31 | CREATE_COMPLETE: 'CREATE_COMPLETE', 32 | FAILED: 'FAILED' 33 | }; 34 | 35 | // By no means a full list, just the ones we care about. 36 | exports.resourceStatus = { 37 | CREATE_COMPLETE: 'CREATE_COMPLETE', 38 | CREATE_FAILED: 'CREATE_FAILED', 39 | DELETE_COMPLETE: 'DELETE_COMPLETE', 40 | DELETE_FAILED: 'DELETE_FAILED', 41 | UPDATE_COMPLETE: 'UPDATE_COMPLETE', 42 | UPDATE_ROLLBACK_COMPLETE: 'UPDATE_ROLLBACK_COMPLETE', 43 | UPDATE_ROLLBACK_FAILED: 'UPDATE_ROLLBACK_FAILED' 44 | }; 45 | 46 | exports.resourceType = { 47 | STACK: 'AWS::CloudFormation::Stack' 48 | }; 49 | 50 | // Tag names used internally. 51 | exports.tag = { 52 | STACK_BASE_NAME: 'cloudformation-deploy:stackBaseName', 53 | STACK_NAME: 'cloudformation-deploy:stackName', 54 | VERSION: 'cloudformation-deploy:version' 55 | }; 56 | 57 | // Type of operation. 58 | exports.type = { 59 | CREATE_STACK: 'create', 60 | DELETE_STACK: 'delete', 61 | UPDATE_STACK: 'update' 62 | }; 63 | -------------------------------------------------------------------------------- /lib/deploy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Main deploy class definition. 3 | */ 4 | 5 | // Core. 6 | var util = require('util'); 7 | 8 | // NPM. 9 | var async = require('async'); 10 | var _ = require('lodash'); 11 | 12 | // Local. 13 | var CloudFormationOperation = require('./cloudFormationOperation'); 14 | var configValidator = require('./configValidator'); 15 | var constants = require('./constants'); 16 | var utilities = require('./utilities'); 17 | 18 | // -------------------------------------------------------------------------- 19 | // Class definition. 20 | // -------------------------------------------------------------------------- 21 | 22 | /** 23 | * @class Deployment interface class. 24 | * 25 | * @param {Object} config Configuration object. 26 | * @param {Object|String} template The CloudFormation template as either an 27 | * object or JSON string, or a URL to a template file in S3 in the same region 28 | * as the stack will be deployed to. 29 | */ 30 | function Deploy (config, template) { 31 | Deploy.super_.call(this, config, template); 32 | this.config = utilities.fillDeployConfigurationDefaults(this.config); 33 | } 34 | util.inherits(Deploy, CloudFormationOperation); 35 | 36 | // ------------------------------------------------------------------------- 37 | // Methods. 38 | // ------------------------------------------------------------------------- 39 | 40 | /** 41 | * Delete a stack and wait on its completion. 42 | * 43 | * Add events to the objects provided, and call back with an error on either a 44 | * timeout or failure of stack completion. 45 | * 46 | * @param {Object} stackData The unique name of the stack to wait on. 47 | * @param {Function} callback Of the form function (error). 48 | */ 49 | Deploy.prototype.deleteStack = function (stackData, callback) { 50 | var self = this; 51 | 52 | async.series({ 53 | // Start the deletion underway. 54 | deleteStack: function (asyncCallback) { 55 | self.cloudFormation.deleteStack(stackData.stackId, asyncCallback); 56 | }, 57 | // Wait for it to complete. 58 | awaitCompletion: function (asyncCallback) { 59 | self.awaitCompletion( 60 | constants.type.DELETE_STACK, 61 | stackData, 62 | asyncCallback 63 | ); 64 | } 65 | }, callback); 66 | }; 67 | 68 | /** 69 | * Delete all stacks with tags showing them to be earlier versions of the newly 70 | * created stack. 71 | * 72 | * @param {Object} result The result object, containing all the needed data. 73 | * @param {Function} callback Of the form function (error). 74 | */ 75 | Deploy.prototype.deletePriorStacks = function (result, callback) { 76 | var self = this; 77 | 78 | this.cloudFormation.describePriorStacks( 79 | this.config.baseName, 80 | result.createStack.stackId, 81 | function (error, stackDescriptions) { 82 | if (error) { 83 | return callback(error); 84 | } 85 | 86 | if (!stackDescriptions.length) { 87 | return callback(); 88 | } 89 | 90 | // Running in series just in case. There should only be one or two of 91 | // these prior stacks. 92 | async.eachSeries( 93 | stackDescriptions, 94 | function (stackDescription, asyncCallback) { 95 | var stackData = self.getStackData( 96 | stackDescription.StackName, 97 | stackDescription.StackId 98 | ); 99 | result.deleteStack.push(stackData); 100 | self.deleteStack(stackData, asyncCallback); 101 | }, 102 | callback 103 | ); 104 | } 105 | ); 106 | }; 107 | 108 | /** 109 | * Deploy the specified CloudFormation stack. 110 | * 111 | * See the documentation for the form of the config object. 112 | * 113 | * @param {Function} callback Of the form function (error, result). 114 | */ 115 | Deploy.prototype.deploy = function (callback) { 116 | var self = this; 117 | var result = { 118 | errors: [], 119 | createStack: this.getStackData(utilities.determineStackName(this.config)), 120 | describeStack: undefined, 121 | deleteStack: [] 122 | }; 123 | 124 | callback = _.once(callback); 125 | 126 | // ------------------------------------------------------------------------ 127 | // Run the stages of the deployment. 128 | // ------------------------------------------------------------------------ 129 | 130 | async.series({ 131 | // Validate the configuration we've been provided. 132 | validateConfig: function (asyncCallback) { 133 | var errors = configValidator.validateDeployConfig(self.config); 134 | if (errors.length) { 135 | asyncCallback(new Error(JSON.stringify(errors))); 136 | } 137 | else { 138 | asyncCallback(); 139 | } 140 | }, 141 | 142 | // Validate the template. 143 | validateTemplate: function (asyncCallback) { 144 | self.cloudFormation.validateTemplate(self.template, asyncCallback); 145 | }, 146 | 147 | // Start the stack creation rolling. 148 | createStack: function (asyncCallback) { 149 | self.cloudFormation.createStack(self.template, function (error, data) { 150 | if (error) { 151 | return asyncCallback(error); 152 | } 153 | 154 | result.createStack.stackId = data.StackId; 155 | asyncCallback(); 156 | }); 157 | }, 158 | 159 | // Wait for the stack creation to complete, fail, or timeout, and report on 160 | // errors and events along the way. 161 | awaitCompletion: function (asyncCallback) { 162 | var stackData = result.createStack; 163 | self.awaitCompletion( 164 | constants.type.CREATE_STACK, 165 | stackData, 166 | function (error) { 167 | if (error) { 168 | // Improve on the error messages where possible by adding more 169 | // context. 170 | if ( 171 | self.config.onDeployFailure === constants.onDeployFailure.DO_NOTHING && 172 | stackData.status === constants.resourceStatus.CREATE_FAILED 173 | ) { 174 | asyncCallback(new Error(util.format( 175 | 'Stack creation failed. Per configuration no attempt was made to delete the failed stack: %s', 176 | error.stack 177 | ))); 178 | } 179 | else if (stackData.status === constants.resourceStatus.DELETE_FAILED) { 180 | asyncCallback(new Error(util.format( 181 | 'Stack creation failed. Deletion of the stack failed as well: %s', 182 | error.stack 183 | ))); 184 | } 185 | else if (stackData.status === constants.resourceStatus.DELETE_COMPLETE) { 186 | asyncCallback(new Error(util.format( 187 | 'Stack creation failed. The failed stack was deleted: %s', 188 | error.stack 189 | ))); 190 | } 191 | else { 192 | asyncCallback(error); 193 | } 194 | } 195 | else { 196 | asyncCallback(); 197 | } 198 | } 199 | ); 200 | }, 201 | 202 | // Get a stack description, which will contain values set in the Outputs 203 | // section of the CloudFormation template. 204 | describeStack: function (asyncCallback) { 205 | self.cloudFormation.describeStack( 206 | result.createStack.stackId, 207 | function (error, description) { 208 | result.describeStack = description; 209 | asyncCallback(error); 210 | } 211 | ); 212 | }, 213 | 214 | // If we have a config.postCreationFn then make use of it now that the 215 | // stack creation is successful. 216 | postCreationFn: function (asyncCallback) { 217 | if (typeof self.config.postCreationFn !== 'function') { 218 | return asyncCallback(); 219 | } 220 | 221 | self.config.postCreationFn(result.describeStack, asyncCallback); 222 | }, 223 | 224 | // If configuration is set to delete prior stacks, then find them by tag 225 | // values and delete them. 226 | deletePriorStacks: function (asyncCallback) { 227 | if (self.config.priorInstance !== constants.priorInstance.DELETE) { 228 | return asyncCallback(); 229 | } 230 | 231 | self.deletePriorStacks(result, asyncCallback); 232 | } 233 | }, function (error) { 234 | if (error) { 235 | result.errors.push(error); 236 | } 237 | 238 | // Always send back the result regardless. 239 | callback(error, result); 240 | }); 241 | }; 242 | 243 | // -------------------------------------------------------------------------- 244 | // Exports constructor. 245 | // -------------------------------------------------------------------------- 246 | 247 | module.exports = Deploy; 248 | -------------------------------------------------------------------------------- /lib/previewUpdate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Main preview update class definition. 3 | */ 4 | 5 | // Core. 6 | var util = require('util'); 7 | 8 | // NPM. 9 | var async = require('async'); 10 | var _ = require('lodash'); 11 | 12 | // Local. 13 | var CloudFormationOperation = require('./cloudFormationOperation'); 14 | var configValidator = require('./configValidator'); 15 | var constants = require('./constants'); 16 | var utilities = require('./utilities'); 17 | 18 | // -------------------------------------------------------------------------- 19 | // Class definition. 20 | // -------------------------------------------------------------------------- 21 | 22 | /** 23 | * @class PreviewUpdate interface class. 24 | * 25 | * @param {Object} config Configuration object. 26 | * @param {Object|String} template The CloudFormation template as either an 27 | * object or JSON string, or a URL to a template file in S3 in the same region 28 | * as the stack will be deployed to. 29 | */ 30 | function PreviewUpdate (config, template) { 31 | PreviewUpdate.super_.call(this, config, template); 32 | this.config = utilities.fillPreviewUpdateConfigurationDefaults(this.config); 33 | } 34 | util.inherits(PreviewUpdate, CloudFormationOperation); 35 | 36 | // ------------------------------------------------------------------------- 37 | // Methods. 38 | // ------------------------------------------------------------------------- 39 | 40 | /** 41 | * Wait on the completion of a changeset creation operation. Return the 42 | * changeset description once done. 43 | * 44 | * @param {Function} callback Of the form function (error, data). 45 | */ 46 | PreviewUpdate.prototype.awaitCompletion = function (callback) { 47 | var self = this; 48 | var changeSet; 49 | 50 | callback = _.once(callback); 51 | 52 | /** 53 | * Which status to watch for completion depends on whether or not 54 | * configuration is set to delete a failed stack automatically. 55 | * 56 | * @return {Boolean} True if complete. 57 | */ 58 | function isComplete () { 59 | if (!changeSet) { 60 | return false; 61 | } 62 | 63 | return _.includes([ 64 | constants.changeSetStatus.CREATE_COMPLETE, 65 | constants.changeSetStatus.FAILED 66 | ], changeSet.Status); 67 | } 68 | 69 | async.until( 70 | // Truth test - continue running the next function argument until this test 71 | // returns true. 72 | isComplete, 73 | 74 | // Wait for the progress check interval then load the changeset. 75 | function (asyncCallback) { 76 | setTimeout(function () { 77 | self.cloudFormation.describeChangeSet(function (error, data) { 78 | changeSet = data || changeSet; 79 | asyncCallback(error); 80 | }); 81 | }, self.config.progressCheckIntervalInSeconds * 1000); 82 | }, 83 | 84 | // Once the truth test returns true, or an error is generated, then here we 85 | // are. 86 | function (error) { 87 | if (error) { 88 | return callback(error); 89 | } 90 | 91 | callback(null, changeSet); 92 | } 93 | ); 94 | }; 95 | 96 | /** 97 | * Preview an update of the specified CloudFormation stack. 98 | * 99 | * See the documentation for the form of the config object. 100 | * 101 | * @param {Function} callback Of the form function (error, result). 102 | */ 103 | PreviewUpdate.prototype.previewUpdate = function (callback) { 104 | var self = this; 105 | var result = { 106 | errors: [], 107 | changeSet: undefined 108 | }; 109 | 110 | callback = _.once(callback); 111 | 112 | // ------------------------------------------------------------------------ 113 | // Run the stages of the preview. 114 | // ------------------------------------------------------------------------ 115 | 116 | async.series({ 117 | // Validate the configuration we've been provided. 118 | validateConfig: function (asyncCallback) { 119 | var errors = configValidator.validatePreviewUpdateConfig(self.config); 120 | if (errors.length) { 121 | asyncCallback(new Error(JSON.stringify(errors))); 122 | } 123 | else { 124 | asyncCallback(); 125 | } 126 | }, 127 | 128 | // Validate the template. 129 | validateTemplate: function (asyncCallback) { 130 | self.cloudFormation.validateTemplate(self.template, asyncCallback); 131 | }, 132 | 133 | // Start creation of the changeset. 134 | createChangeSet: function (asyncCallback) { 135 | self.cloudFormation.createChangeSet(self.template, function (error, data) { 136 | if (error) { 137 | return asyncCallback(error); 138 | } 139 | 140 | asyncCallback(); 141 | }); 142 | }, 143 | 144 | // Wait for the creation of the changeset to succeed or fail. 145 | awaitCompletion: function (asyncCallback) { 146 | self.awaitCompletion(function (error, changeSet) { 147 | result.changeSet = changeSet; 148 | asyncCallback(error); 149 | }); 150 | }, 151 | 152 | deleteChangeSet: function (asyncCallback) { 153 | if (!self.config.deleteChangeSet) { 154 | return asyncCallback(); 155 | } 156 | 157 | self.cloudFormation.deleteChangeSet(asyncCallback); 158 | } 159 | }, function (error) { 160 | if (error) { 161 | result.errors.push(error); 162 | } 163 | 164 | // Always send back the result regardless. 165 | callback(error, result); 166 | }); 167 | }; 168 | 169 | // -------------------------------------------------------------------------- 170 | // Exports constructor. 171 | // -------------------------------------------------------------------------- 172 | 173 | module.exports = PreviewUpdate; 174 | -------------------------------------------------------------------------------- /lib/update.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Main update class definition. 3 | */ 4 | 5 | // Core. 6 | var util = require('util'); 7 | 8 | // NPM. 9 | var async = require('async'); 10 | var _ = require('lodash'); 11 | 12 | // Local. 13 | var CloudFormationOperation = require('./cloudFormationOperation'); 14 | var configValidator = require('./configValidator'); 15 | var constants = require('./constants'); 16 | var utilities = require('./utilities'); 17 | 18 | // -------------------------------------------------------------------------- 19 | // Class definition. 20 | // -------------------------------------------------------------------------- 21 | 22 | /** 23 | * @class Update interface class. 24 | * 25 | * @param {Object} config Configuration object. 26 | * @param {Object|String} template The CloudFormation template as either an 27 | * object or JSON string, or a URL to a template file in S3 in the same region 28 | * as the stack will be deployed to. 29 | */ 30 | function Update (config, template) { 31 | Update.super_.call(this, config, template); 32 | this.config = utilities.fillUpdateConfigurationDefaults(this.config); 33 | } 34 | util.inherits(Update, CloudFormationOperation); 35 | 36 | // ------------------------------------------------------------------------- 37 | // Methods. 38 | // ------------------------------------------------------------------------- 39 | 40 | /** 41 | * Update the specified CloudFormation stack. 42 | * 43 | * See the documentation for the form of the config object. 44 | * 45 | * @param {Function} callback Of the form function (error, result). 46 | */ 47 | Update.prototype.update = function (callback) { 48 | var self = this; 49 | var result = { 50 | errors: [], 51 | updateStack: this.getStackData(utilities.determineStackName(this.config)), 52 | describeStack: undefined 53 | }; 54 | 55 | callback = _.once(callback); 56 | 57 | // ------------------------------------------------------------------------ 58 | // Run the stages of the update. 59 | // ------------------------------------------------------------------------ 60 | 61 | async.series({ 62 | // Validate the configuration we've been provided. 63 | validateConfig: function (asyncCallback) { 64 | var errors = configValidator.validateUpdateConfig(self.config); 65 | if (errors.length) { 66 | asyncCallback(new Error(JSON.stringify(errors))); 67 | } 68 | else { 69 | asyncCallback(); 70 | } 71 | }, 72 | 73 | // Validate the template. 74 | validateTemplate: function (asyncCallback) { 75 | self.cloudFormation.validateTemplate(self.template, asyncCallback); 76 | }, 77 | 78 | // Start the stack update rolling. 79 | updateStack: function (asyncCallback) { 80 | self.cloudFormation.updateStack(self.template, function (error, data) { 81 | if (error) { 82 | return asyncCallback(error); 83 | } 84 | 85 | result.updateStack.stackId = data.StackId; 86 | asyncCallback(); 87 | }); 88 | }, 89 | 90 | // Wait for the stack update to complete or fail. Report on errors and 91 | // events along the way. 92 | awaitCompletion: function (asyncCallback) { 93 | var stackData = result.updateStack; 94 | self.awaitCompletion( 95 | constants.type.UPDATE_STACK, 96 | stackData, 97 | function (error) { 98 | if (error) { 99 | // Improve on the error messages where possible by adding more 100 | // context. 101 | if (stackData.status === constants.resourceStatus.UPDATE_ROLLBACK_COMPLETE) { 102 | asyncCallback(new Error(util.format( 103 | 'Stack update failed. The rollback succeeded: %s', 104 | error.stack 105 | ))); 106 | } 107 | else if (stackData.status === constants.resourceStatus.UPDATE_ROLLBACK_FAILED) { 108 | asyncCallback(new Error(util.format( 109 | 'Stack update failed. The rollback failed as well: %s', 110 | error.stack 111 | ))); 112 | } 113 | else { 114 | asyncCallback(error); 115 | } 116 | } 117 | else { 118 | asyncCallback(); 119 | } 120 | } 121 | ); 122 | }, 123 | 124 | // Get a stack description, which will contain values set in the Outputs 125 | // section of the CloudFormation template. 126 | describeStack: function (asyncCallback) { 127 | self.cloudFormation.describeStack( 128 | result.updateStack.stackId, 129 | function (error, description) { 130 | result.describeStack = description; 131 | asyncCallback(error); 132 | } 133 | ); 134 | } 135 | }, function (error) { 136 | if (error) { 137 | result.errors.push(error); 138 | } 139 | 140 | // Always send back the result regardless. 141 | callback(error, result); 142 | }); 143 | }; 144 | 145 | // -------------------------------------------------------------------------- 146 | // Exports constructor. 147 | // -------------------------------------------------------------------------- 148 | 149 | module.exports = Update; 150 | -------------------------------------------------------------------------------- /lib/utilities.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Various utility functions. 3 | */ 4 | 5 | // NPM. 6 | var _ = require('lodash'); 7 | 8 | // Local. 9 | var constants = require('./constants'); 10 | 11 | /** 12 | * Obtain the stack name from config. 13 | * 14 | * @param {Object} config Configuration. 15 | * @return {String} The stack name. 16 | */ 17 | exports.determineStackName = function (config) { 18 | // If an update configuration, stack name is defined. 19 | if (config.stackName) { 20 | return config.stackName; 21 | } 22 | 23 | // Otherwise it is a deploy config, and construct the name from other values. 24 | var stackName = config.baseName + '-' + config.deployId; 25 | 26 | // All invalid characters are replaced with dashes. 27 | return stackName.replace(/[^\-a-z0-9]/ig, '-'); 28 | }; 29 | 30 | /** 31 | * Does this baseName match the baseName portion of this stackName? 32 | * 33 | * @param {String} baseName A base name. 34 | * @param {String} stackName A stack name. 35 | * @return {Boolean} True if there is a match. 36 | */ 37 | exports.baseNameMatchesStackName = function (baseName, stackName) { 38 | return stackName.indexOf(baseName + '-') === 0; 39 | }; 40 | 41 | /** 42 | * Get an array of parameter definitions suitable for use with the AWS-SDK 43 | * client. 44 | * 45 | * @param {Object} config Configuration. 46 | * @param {Object[]} Array of parameter definitions. 47 | */ 48 | exports.getParameters = function (config) { 49 | return _.map(config.parameters, function (value, key) { 50 | return { 51 | ParameterKey: key, 52 | ParameterValue: value 53 | }; 54 | }); 55 | }; 56 | 57 | /** 58 | * Get an array of tag definitions suitable for use with the AWS-SDK client. 59 | * 60 | * @param {Object} config Configuration. 61 | * @param {Object[]} Array of tag definitions. 62 | */ 63 | exports.getTags = function (config) { 64 | var tags = _.map(config.tags, function (value, key) { 65 | return { 66 | Key: key, 67 | Value: value 68 | }; 69 | }); 70 | 71 | tags.push({ 72 | Key: constants.tag.STACK_NAME, 73 | Value: exports.determineStackName(config) 74 | }); 75 | 76 | if (config.baseName) { 77 | tags.push({ 78 | Key: constants.tag.STACK_BASE_NAME, 79 | Value: config.baseName 80 | }); 81 | } 82 | 83 | if (config.version) { 84 | tags.push({ 85 | Key: constants.tag.VERSION, 86 | Value: config.version 87 | }); 88 | } 89 | 90 | return tags; 91 | }; 92 | 93 | /** 94 | * Given a parameters object for a CloudFormation request, add either a 95 | * TemplateURL or TemplateBody property depending on what has been passed as the 96 | * template. 97 | * 98 | * @param {Object} params Parameters object. 99 | * @param {Object|String} template Either an object, JSON, or a URL. 100 | */ 101 | exports.addTemplatePropertyToParameters = function (params, template) { 102 | if (typeof template === 'string') { 103 | // Is the template a url? 104 | if (template.match(/^https?:\/\/.+/)) { 105 | params.TemplateURL = template; 106 | } 107 | else { 108 | params.TemplateBody = template; 109 | } 110 | } 111 | else { 112 | params.TemplateBody = JSON.stringify(template); 113 | } 114 | }; 115 | 116 | /** 117 | * Fill out the deployment configuration object with default values. 118 | * 119 | * @param {Object} config Configuration. 120 | * @param {Object} Configuration with defaults set. 121 | */ 122 | exports.fillDeployConfigurationDefaults = function (config) { 123 | return _.defaults(config, { 124 | clientOptions: undefined, 125 | capabilities: _.values(constants.capabilities), 126 | createStackTimeoutInMinutes: 10, 127 | tags: {}, 128 | parameters: {}, 129 | progressCheckIntervalInSeconds: 10, 130 | onEventFn: function () {}, 131 | postCreationFn: function (stackDescription, callback) { 132 | callback(); 133 | }, 134 | priorInstance: constants.priorInstance.DELETE, 135 | onDeployFailure: constants.onDeployFailure.DELETE 136 | }); 137 | }; 138 | 139 | /** 140 | * Fill out the preview update configuration object with default values. 141 | * 142 | * @param {Object} config Configuration. 143 | * @param {Object} Configuration with defaults set. 144 | */ 145 | exports.fillPreviewUpdateConfigurationDefaults = function (config) { 146 | return _.defaults(config, { 147 | clientOptions: undefined, 148 | capabilities: _.values(constants.capabilities), 149 | deleteChangeSet: true, 150 | tags: {}, 151 | parameters: {}, 152 | progressCheckIntervalInSeconds: 10 153 | }); 154 | }; 155 | 156 | /** 157 | * Fill out the update configuration object with default values. 158 | * 159 | * @param {Object} config Configuration. 160 | * @param {Object} Configuration with defaults set. 161 | */ 162 | exports.fillUpdateConfigurationDefaults = function (config) { 163 | return _.defaults(config, { 164 | clientOptions: undefined, 165 | capabilities: _.values(constants.capabilities), 166 | tags: {}, 167 | parameters: {}, 168 | progressCheckIntervalInSeconds: 10, 169 | onEventFn: function () {} 170 | }); 171 | }; 172 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudformation-deploy", 3 | "description": "Wrapping deployment to AWS via CloudFormation template.", 4 | "keywords": [ 5 | "aws", 6 | "cloudformation", 7 | "deploy" 8 | ], 9 | "version": "0.5.1", 10 | "homepage": "https://github.com/exratione/cloudformation-deploy", 11 | "author": "Reason ", 12 | "engines": { 13 | "node": ">= 12.0.0" 14 | }, 15 | "dependencies": { 16 | "async": "2.4.1", 17 | "aws-sdk": "2.48.0", 18 | "chai": "3.1.0", 19 | "grunt": "1.0.4", 20 | "grunt-eslint": "22.0.0", 21 | "grunt-mocha-test": "0.13.3", 22 | "jsonschema": "1.0.2", 23 | "lodash": "4.17.15", 24 | "mocha": "6.2.2", 25 | "sinon": "1.15.4" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/exratione/cloudformation-deploy" 30 | }, 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "assert": true, 7 | "expect": true, 8 | "should": true, 9 | "sinon": true 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Tests for index.js 3 | */ 4 | 5 | // Local. 6 | var constants = require('../lib/constants'); 7 | var Deploy = require('../lib/deploy'); 8 | var index = require('../index'); 9 | var PreviewUpdate = require('../lib/previewUpdate'); 10 | var Update = require('../lib/update'); 11 | 12 | describe('index', function () { 13 | var sandbox; 14 | 15 | beforeEach(function () { 16 | sandbox = sinon.sandbox.create(); 17 | }); 18 | 19 | afterEach(function () { 20 | sandbox.restore(); 21 | }); 22 | 23 | describe('capabilities', function () { 24 | it('is correctly assigned', function () { 25 | expect(index.capabilities).to.equal(constants.capabilities); 26 | }); 27 | }); 28 | 29 | describe('onDeployFailure', function () { 30 | it('is correctly assigned', function () { 31 | expect(index.onDeployFailure).to.equal(constants.onDeployFailure); 32 | }); 33 | }); 34 | 35 | describe('priorInstance', function () { 36 | it('is correctly assigned', function () { 37 | expect(index.priorInstance).to.equal(constants.priorInstance); 38 | }); 39 | }); 40 | 41 | describe('deploy', function () { 42 | var config; 43 | var template; 44 | 45 | beforeEach(function () { 46 | config = {}; 47 | template = {}; 48 | 49 | sandbox.stub(Deploy.prototype, 'deploy').yields(); 50 | }); 51 | 52 | it('functions as expected', function (done) { 53 | index.deploy(config, template, function (error) { 54 | sinon.assert.calledWith( 55 | Deploy.prototype.deploy, 56 | sinon.match.func 57 | ); 58 | 59 | done(error); 60 | }); 61 | }) 62 | }); 63 | 64 | describe('previewUpdate', function () { 65 | var config; 66 | var template; 67 | 68 | beforeEach(function () { 69 | config = {}; 70 | template = {}; 71 | 72 | sandbox.stub(PreviewUpdate.prototype, 'previewUpdate').yields(); 73 | }); 74 | 75 | it('functions as expected', function (done) { 76 | index.previewUpdate(config, template, function (error) { 77 | sinon.assert.calledWith( 78 | PreviewUpdate.prototype.previewUpdate, 79 | sinon.match.func 80 | ); 81 | 82 | done(error); 83 | }); 84 | }) 85 | }); 86 | 87 | describe('update', function () { 88 | var config; 89 | var template; 90 | 91 | beforeEach(function () { 92 | config = {}; 93 | template = {}; 94 | 95 | sandbox.stub(Update.prototype, 'update').yields(); 96 | }); 97 | 98 | it('functions as expected', function (done) { 99 | index.update(config, template, function (error) { 100 | sinon.assert.calledWith( 101 | Update.prototype.update, 102 | sinon.match.func 103 | ); 104 | 105 | done(error); 106 | }); 107 | }) 108 | }); 109 | 110 | }); 111 | -------------------------------------------------------------------------------- /test/lib/cloudFormation.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Tests for CloudFormation utilities. 3 | */ 4 | 5 | // NPM. 6 | var AWS = require('aws-sdk'); 7 | 8 | // Local. 9 | var CloudFormation = require('../../lib/cloudFormation'); 10 | var constants = require('../../lib/constants'); 11 | var resources = require('../resources'); 12 | var utilities = require('../../lib/utilities'); 13 | 14 | describe('lib/cloudFormation', function () { 15 | var cloudFormation; 16 | var config; 17 | var sandbox; 18 | var template; 19 | 20 | function stubClient (client) { 21 | // Make sure we stub everything that is used. 22 | sandbox.stub(client, 'createChangeSet').yields(null, { 23 | Id: '', 24 | StackId: '' 25 | }); 26 | sandbox.stub(client, 'createStack').yields(null, { 27 | StackId: '' 28 | }); 29 | sandbox.stub(client, 'updateStack').yields(null, { 30 | StackId: '' 31 | }); 32 | sandbox.stub(client, 'deleteChangeSet').yields(); 33 | sandbox.stub(client, 'deleteStack').yields(); 34 | sandbox.stub(client, 'describeChangeSet').yields(); 35 | sandbox.stub(client, 'describeStacks').yields(null, { 36 | Stacks: [{}] 37 | }); 38 | sandbox.stub(client, 'describeStackEvents').yields(null, { 39 | StackEvents: [] 40 | }); 41 | sandbox.stub(client, 'listStacks').yields(null, [{}]); 42 | sandbox.stub(client, 'validateTemplate').yields(); 43 | } 44 | 45 | beforeEach(function () { 46 | sandbox = sinon.sandbox.create(); 47 | template = JSON.stringify({}); 48 | }); 49 | 50 | afterEach(function () { 51 | sandbox.restore(); 52 | }); 53 | 54 | describe('general operations', function () { 55 | 56 | beforeEach(function () { 57 | config = resources.getDeployConfig(); 58 | cloudFormation = new CloudFormation(config); 59 | stubClient(cloudFormation.client); 60 | }); 61 | 62 | describe('client', function () { 63 | it('creates a client with implicit configuration', function () { 64 | expect(cloudFormation.client).to.be.instanceOf(AWS.CloudFormation); 65 | }); 66 | 67 | it('creates a client with explicit configuration', function () { 68 | sandbox.spy(AWS, 'CloudFormation'); 69 | config.clientOptions = { 70 | region: 'eu-west-1' 71 | }; 72 | cloudFormation = new CloudFormation(config); 73 | sinon.assert.calledWith(AWS.CloudFormation, config.clientOptions); 74 | }); 75 | }); 76 | 77 | describe('validateTemplate', function () { 78 | it('invokes validateTemplate with expected arguments', function (done) { 79 | cloudFormation.validateTemplate(template, function (error) { 80 | sinon.assert.calledWith( 81 | cloudFormation.client.validateTemplate, 82 | { 83 | TemplateBody: template 84 | }, 85 | sinon.match.func 86 | ); 87 | 88 | done(error); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('describeStack', function () { 94 | it('invokes describeStacks with expected arguments', function (done) { 95 | var stackId = ''; 96 | 97 | cloudFormation.describeStack(stackId, function (error, data) { 98 | sinon.assert.calledWith( 99 | cloudFormation.client.describeStacks, 100 | { 101 | StackName: stackId 102 | }, 103 | sinon.match.func 104 | ); 105 | 106 | expect(data).to.eql({}); 107 | done(error); 108 | }); 109 | }); 110 | 111 | it('calls back with error if empty stacks array returned', function (done) { 112 | var stackId = ''; 113 | cloudFormation.client.describeStacks.yields(null, { 114 | Stacks: [] 115 | }); 116 | 117 | cloudFormation.describeStack(stackId, function (error, data) { 118 | expect(error).to.be.instanceof(Error); 119 | done(); 120 | }); 121 | }); 122 | }); 123 | }); 124 | 125 | describe('deploy operations', function () { 126 | 127 | beforeEach(function () { 128 | config = resources.getDeployConfig(); 129 | cloudFormation = new CloudFormation(config); 130 | stubClient(cloudFormation.client); 131 | }); 132 | 133 | describe('createStack', function () { 134 | it('invokes createStack with expected arguments', function (done) { 135 | cloudFormation.createStack(template, function (error, data) { 136 | sinon.assert.calledWith( 137 | cloudFormation.client.createStack, 138 | { 139 | StackName: utilities.determineStackName(config), 140 | Capabilities: config.capabilities, 141 | OnFailure: config.onDeployFailure, 142 | Parameters: utilities.getParameters(config), 143 | Tags: utilities.getTags(config), 144 | TemplateBody: template, 145 | TimeoutInMinutes: config.createStackTimeoutInMinutes 146 | }, 147 | sinon.match.func 148 | ); 149 | 150 | expect(data).to.eql({ 151 | StackId: '' 152 | }); 153 | 154 | done(error); 155 | }); 156 | }); 157 | }); 158 | 159 | describe('deleteStack', function () { 160 | it('invokes deleteStack with expected arguments', function (done) { 161 | var stackId = ''; 162 | cloudFormation.deleteStack(stackId, function (error) { 163 | sinon.assert.calledWith( 164 | cloudFormation.client.deleteStack, 165 | { 166 | StackName: stackId 167 | }, 168 | sinon.match.func 169 | ); 170 | 171 | done(error); 172 | }); 173 | }); 174 | }); 175 | 176 | describe('describePriorStacks', function () { 177 | var createdStackId = 'stackId'; 178 | 179 | it('functions correctly when no stacks are returned from listStacks', function (done) { 180 | cloudFormation.describePriorStacks( 181 | config.baseName, 182 | createdStackId, 183 | function (error, stackDescriptions) { 184 | sinon.assert.calledWith( 185 | cloudFormation.client.listStacks, 186 | { 187 | StackStatusFilter: cloudFormation.stackStatusFilter 188 | }, 189 | sinon.match.func 190 | ); 191 | 192 | expect(stackDescriptions).to.eql([]); 193 | done(error); 194 | } 195 | ); 196 | }); 197 | 198 | it('makes repeated listStacks requests for NextToken', function (done) { 199 | var validPriorConfig = resources.getDeployConfig({ 200 | deployId: 'random-valid' 201 | }); 202 | var invalidPriorConfig = resources.getDeployConfig({ 203 | deployId: 'random-invalid' 204 | }); 205 | 206 | var validPriorStackSummary = { 207 | StackName: utilities.determineStackName(validPriorConfig), 208 | StackId: 'priorStackId' 209 | }; 210 | var invalidPriorStackSummary = { 211 | StackName: utilities.determineStackName(invalidPriorConfig), 212 | StackId: 'priorStackId' 213 | }; 214 | var createdStackSummary = { 215 | StackName: utilities.determineStackName(config), 216 | StackId: createdStackId 217 | }; 218 | 219 | var validPriorStackDescription = { 220 | Tags: [ 221 | { 222 | Key: constants.tag.STACK_BASE_NAME, 223 | Value: config.baseName 224 | } 225 | ] 226 | }; 227 | var invalidPriorStackDescription = { 228 | Tags: [ 229 | { 230 | Key: constants.tag.STACK_BASE_NAME, 231 | Value: 'another tag value' 232 | } 233 | ] 234 | }; 235 | 236 | // The client returns events in reverse chronological order. 237 | cloudFormation.client.listStacks.onCall(0).yields(null, { 238 | NextToken: 'call0', 239 | StackSummaries: [ 240 | { 241 | StackName: 'sn-1', 242 | StackId: 'id-1' 243 | }, 244 | validPriorStackSummary 245 | ] 246 | }); 247 | cloudFormation.client.listStacks.onCall(1).yields(null, { 248 | NextToken: 'call1', 249 | StackSummaries: [ 250 | invalidPriorStackSummary, 251 | { 252 | StackName: 'sn-4', 253 | StackId: 'id-4' 254 | } 255 | ] 256 | }); 257 | cloudFormation.client.listStacks.onCall(2).yields(null, { 258 | StackSummaries: [ 259 | { 260 | StackName: 'sn-3', 261 | StackId: 'id-3' 262 | }, 263 | createdStackSummary 264 | ] 265 | }); 266 | 267 | // Set this up so that only one of the two possible prior stacks has the 268 | // right matching tag. 269 | sandbox.stub(cloudFormation, 'describeStack'); 270 | cloudFormation.describeStack.onCall(0).yields(null, validPriorStackDescription); 271 | cloudFormation.describeStack.onCall(1).yields(null, invalidPriorStackDescription); 272 | 273 | cloudFormation.describePriorStacks( 274 | config.baseName, 275 | createdStackId, 276 | function (error, stackDescriptions) { 277 | sinon.assert.callCount(cloudFormation.client.listStacks, 3); 278 | cloudFormation.client.listStacks.getCall(0).calledWith( 279 | { 280 | StackStatusFilter: cloudFormation.stackStatusFilter 281 | }, 282 | sinon.match.func 283 | ); 284 | cloudFormation.client.listStacks.getCall(1).calledWith( 285 | cloudFormation.client.describeStackEvents, 286 | { 287 | NextToken: 'call0', 288 | StackStatusFilter: cloudFormation.stackStatusFilter 289 | }, 290 | sinon.match.func 291 | ); 292 | cloudFormation.client.listStacks.getCall(2).calledWith( 293 | cloudFormation.client.describeStackEvents, 294 | { 295 | NextToken: 'call1', 296 | StackStatusFilter: cloudFormation.stackStatusFilter 297 | }, 298 | sinon.match.func 299 | ); 300 | 301 | sinon.assert.calledTwice(cloudFormation.describeStack); 302 | cloudFormation.describeStack.getCall(0).calledWith( 303 | validPriorStackSummary.StackId, 304 | sinon.match.func 305 | ); 306 | cloudFormation.describeStack.getCall(1).calledWith( 307 | invalidPriorStackSummary.StackId, 308 | sinon.match.func 309 | ); 310 | 311 | expect(stackDescriptions).to.eql([validPriorStackDescription]); 312 | 313 | done(error); 314 | } 315 | ); 316 | }); 317 | }); 318 | 319 | describe('describeStackEvents', function () { 320 | it('invokes describeStackEvents with expected arguments', function (done) { 321 | var stackId = ''; 322 | cloudFormation.describeStackEvents(stackId, function (error, results) { 323 | sinon.assert.calledWith( 324 | cloudFormation.client.describeStackEvents, 325 | { 326 | StackName: stackId 327 | }, 328 | sinon.match.func 329 | ); 330 | 331 | expect(results).to.eql([]); 332 | done(error); 333 | }); 334 | }); 335 | 336 | it('makes repeated requests for NextToken', function (done) { 337 | var stackId = ''; 338 | 339 | // The client returns events in reverse chronological order. 340 | cloudFormation.client.describeStackEvents.onCall(0).yields(null, { 341 | NextToken: 'call0', 342 | StackEvents: [{ i: 6 }, { i: 5 }] 343 | }); 344 | cloudFormation.client.describeStackEvents.onCall(1).yields(null, { 345 | NextToken: 'call1', 346 | StackEvents: [{ i: 4 }, { i: 3 }] 347 | }); 348 | cloudFormation.client.describeStackEvents.onCall(2).yields(null, { 349 | StackEvents: [{ i: 2 }, { i: 1 }] 350 | }); 351 | 352 | cloudFormation.describeStackEvents(stackId, function (error, results) { 353 | sinon.assert.callCount(cloudFormation.client.describeStackEvents, 3); 354 | cloudFormation.client.describeStackEvents.getCall(0).calledWith( 355 | { 356 | StackName: stackId 357 | }, 358 | sinon.match.func 359 | ); 360 | cloudFormation.client.describeStackEvents.getCall(1).calledWith( 361 | cloudFormation.client.describeStackEvents, 362 | { 363 | NextToken: 'call0', 364 | StackName: stackId 365 | }, 366 | sinon.match.func 367 | ); 368 | cloudFormation.client.describeStackEvents.getCall(2).calledWith( 369 | cloudFormation.client.describeStackEvents, 370 | { 371 | NextToken: 'call1', 372 | StackName: stackId 373 | }, 374 | sinon.match.func 375 | ); 376 | 377 | // Events are reversed to put them in chronological order. 378 | expect(results).to.eql([ 379 | { i: 1 }, 380 | { i: 2 }, 381 | { i: 3 }, 382 | { i: 4 }, 383 | { i: 5 }, 384 | { i: 6 } 385 | ]); 386 | 387 | done(error); 388 | }); 389 | }); 390 | }); 391 | }); 392 | 393 | describe('preview update operations', function () { 394 | 395 | beforeEach(function () { 396 | config = resources.getPreviewUpdateConfig(); 397 | cloudFormation = new CloudFormation(config); 398 | stubClient(cloudFormation.client); 399 | }); 400 | 401 | describe('createChangeSet', function () { 402 | it('invokes createChangeSet with expected arguments', function (done) { 403 | cloudFormation.createChangeSet(template, function (error, data) { 404 | sinon.assert.calledWith( 405 | cloudFormation.client.createChangeSet, 406 | { 407 | ChangeSetName: config.changeSetName, 408 | ChangeSetType: 'UPDATE', 409 | StackName: utilities.determineStackName(config), 410 | Capabilities: config.capabilities, 411 | Parameters: utilities.getParameters(config), 412 | Tags: utilities.getTags(config), 413 | TemplateBody: template 414 | }, 415 | sinon.match.func 416 | ); 417 | 418 | expect(data).to.eql({ 419 | Id: '', 420 | StackId: '' 421 | }); 422 | 423 | done(error); 424 | }); 425 | }); 426 | }); 427 | 428 | describe('deleteChangeSet', function () { 429 | it('invokes deleteChangeSet with expected arguments', function (done) { 430 | cloudFormation.deleteChangeSet(function (error) { 431 | sinon.assert.calledWith( 432 | cloudFormation.client.deleteChangeSet, 433 | { 434 | ChangeSetName: config.changeSetName, 435 | StackName: utilities.determineStackName(config) 436 | }, 437 | sinon.match.func 438 | ); 439 | 440 | done(error); 441 | }); 442 | }); 443 | }); 444 | 445 | describe('describeChangeSet', function () { 446 | var dataPage0; 447 | var dataPage1; 448 | 449 | beforeEach(function () { 450 | dataPage0 = { 451 | Changes: [ 452 | {}, 453 | {} 454 | ], 455 | NextToken: 'nextToken' 456 | }; 457 | dataPage1 = { 458 | Changes: [ 459 | {} 460 | ] 461 | }; 462 | 463 | cloudFormation.client.describeChangeSet.onCall(0).yields(null, dataPage0); 464 | cloudFormation.client.describeChangeSet.onCall(1).yields(null, dataPage1); 465 | }); 466 | 467 | 468 | it('invokes describeChangeSet with expected arguments', function (done) { 469 | cloudFormation.describeChangeSet(function (error, data) { 470 | sinon.assert.calledTwice(cloudFormation.client.describeChangeSet); 471 | sinon.assert.calledWith( 472 | cloudFormation.client.describeChangeSet, 473 | sinon.match.object, 474 | sinon.match.func 475 | ); 476 | 477 | expect(cloudFormation.client.describeChangeSet.getCall(0).args[0]).to.eql({ 478 | ChangeSetName: config.changeSetName, 479 | StackName: utilities.determineStackName(config) 480 | }); 481 | expect(cloudFormation.client.describeChangeSet.getCall(1).args[0]).to.eql({ 482 | ChangeSetName: config.changeSetName, 483 | StackName: utilities.determineStackName(config), 484 | NextToken: dataPage0.NextToken 485 | }); 486 | 487 | expect(data).to.eql({ 488 | Changes: [ 489 | {}, 490 | {}, 491 | {} 492 | ] 493 | }); 494 | 495 | done(error); 496 | }); 497 | }); 498 | }); 499 | }); 500 | 501 | describe('update operations', function () { 502 | 503 | beforeEach(function () { 504 | config = resources.getUpdateConfig(); 505 | cloudFormation = new CloudFormation(config); 506 | stubClient(cloudFormation.client); 507 | }); 508 | 509 | describe('updateStack', function () { 510 | it('invokes createStack with expected arguments', function (done) { 511 | cloudFormation.updateStack(template, function (error, data) { 512 | sinon.assert.calledWith( 513 | cloudFormation.client.updateStack, 514 | { 515 | StackName: utilities.determineStackName(config), 516 | Capabilities: config.capabilities, 517 | Parameters: utilities.getParameters(config), 518 | Tags: utilities.getTags(config), 519 | TemplateBody: template 520 | }, 521 | sinon.match.func 522 | ); 523 | 524 | expect(data).to.eql({ 525 | StackId: '' 526 | }); 527 | 528 | done(error); 529 | }); 530 | }); 531 | }); 532 | }); 533 | 534 | }); 535 | -------------------------------------------------------------------------------- /test/lib/cloudFormationOperation.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Tests for the top level cloudFormationOperationment code. 3 | */ 4 | 5 | // Local. 6 | var constants = require('../../lib/constants'); 7 | var CloudFormationOperation = require('../../lib/cloudFormationOperation'); 8 | var resources = require('../resources'); 9 | 10 | describe('lib/cloudFormationOperation', function () { 11 | var config; 12 | var template; 13 | var sandbox; 14 | var stackData; 15 | var stackId; 16 | var stackName; 17 | 18 | beforeEach(function () { 19 | sandbox = sinon.sandbox.create(); 20 | 21 | config = resources.getDeployConfig(); 22 | template = JSON.stringify({}); 23 | cloudFormationOperation = new CloudFormationOperation(config, template); 24 | stackId = 'id'; 25 | stackName = 'name'; 26 | 27 | stackData = cloudFormationOperation.getStackData(stackName, stackId); 28 | 29 | // Stub the event callback. 30 | sandbox.stub(config, 'onEventFn').returns(); 31 | 32 | // Make sure we stub everything that is used. 33 | sandbox.stub(cloudFormationOperation.cloudFormation, 'describeStackEvents').yields(null, []); 34 | }); 35 | 36 | afterEach(function () { 37 | sandbox.restore(); 38 | }); 39 | 40 | describe('getStackData', function () { 41 | it('functions as expected', function () { 42 | expect( 43 | cloudFormationOperation.getStackData(stackName, stackId) 44 | ).to.eql({ 45 | stackName: stackName, 46 | stackId: stackId, 47 | status: undefined, 48 | events: [] 49 | }) 50 | }); 51 | }) 52 | 53 | describe('updateEventData', function () { 54 | var dummyOldEvent; 55 | var dummyNewEvent; 56 | var oldEvents; 57 | var newEvents; 58 | var events; 59 | 60 | beforeEach(function () { 61 | dummyOldEvent = { 62 | ResourceType: 'old', 63 | ResourceStatus: 'old' 64 | }; 65 | dummyNewEvent = { 66 | ResourceType: 'new', 67 | ResourceStatus: 'new' 68 | }; 69 | 70 | oldEvents = [dummyOldEvent, dummyOldEvent]; 71 | 72 | stackData.stackId = stackId; 73 | stackData.events = oldEvents; 74 | }); 75 | 76 | it('functions correctly', function (done) { 77 | newEvents = [ 78 | dummyNewEvent, 79 | { 80 | ResourceType: constants.resourceType.STACK, 81 | ResourceStatus: constants.resourceStatus.CREATE_COMPLETE 82 | }, 83 | dummyNewEvent 84 | ]; 85 | events = oldEvents.concat(newEvents); 86 | 87 | cloudFormationOperation.cloudFormation.describeStackEvents.yields(null, events); 88 | 89 | cloudFormationOperation.updateEventData(stackData, function (error) { 90 | sinon.assert.calledWith( 91 | cloudFormationOperation.cloudFormation.describeStackEvents, 92 | stackData.stackId, 93 | sinon.match.func 94 | ); 95 | 96 | // Each new event should trigger a call to the callback function for 97 | // events. 98 | sinon.assert.callCount(config.onEventFn, newEvents.length); 99 | config.onEventFn.getCall(0).calledWith(newEvents[0]); 100 | config.onEventFn.getCall(1).calledWith(newEvents[1]); 101 | config.onEventFn.getCall(2).calledWith(newEvents[2]); 102 | 103 | expect(stackData.events).to.eql(events); 104 | expect(stackData.status).to.eql(newEvents[1].ResourceStatus); 105 | 106 | done(error); 107 | }); 108 | }); 109 | 110 | it('functions correctly with stack event last', function (done) { 111 | newEvents = [ 112 | dummyNewEvent, 113 | { 114 | ResourceType: constants.resourceType.STACK, 115 | ResourceStatus: constants.resourceStatus.CREATE_COMPLETE 116 | } 117 | ]; 118 | events = oldEvents.concat(newEvents); 119 | 120 | cloudFormationOperation.cloudFormation.describeStackEvents.yields(null, events); 121 | 122 | cloudFormationOperation.updateEventData(stackData, function (error) { 123 | sinon.assert.calledWith( 124 | cloudFormationOperation.cloudFormation.describeStackEvents, 125 | stackData.stackId, 126 | sinon.match.func 127 | ); 128 | 129 | // Each new event should trigger a call to the callback function for 130 | // events. 131 | sinon.assert.callCount(config.onEventFn, newEvents.length); 132 | config.onEventFn.getCall(0).calledWith(newEvents[0]); 133 | config.onEventFn.getCall(1).calledWith(newEvents[1]); 134 | 135 | expect(stackData.events).to.eql(events); 136 | expect(stackData.status).to.eql(newEvents[1].ResourceStatus); 137 | 138 | done(error); 139 | }); 140 | }); 141 | 142 | it('functions correctly with no stack event', function (done) { 143 | newEvents = [ 144 | dummyNewEvent 145 | ]; 146 | events = oldEvents.concat(newEvents); 147 | 148 | cloudFormationOperation.cloudFormation.describeStackEvents.yields(null, events); 149 | 150 | cloudFormationOperation.updateEventData(stackData, function (error) { 151 | sinon.assert.calledWith( 152 | cloudFormationOperation.cloudFormation.describeStackEvents, 153 | stackData.stackId, 154 | sinon.match.func 155 | ); 156 | 157 | // Each new event should trigger a call to the callback function for 158 | // events. 159 | sinon.assert.callCount(config.onEventFn, newEvents.length); 160 | config.onEventFn.getCall(0).calledWith(newEvents[0]); 161 | 162 | expect(stackData.events).to.eql(events); 163 | expect(stackData.status).to.equal(undefined); 164 | 165 | done(error); 166 | }); 167 | }); 168 | 169 | it('calls back with error on error', function (done) { 170 | cloudFormationOperation.cloudFormation.describeStackEvents.yields(new Error()); 171 | 172 | cloudFormationOperation.updateEventData(stackData, function (error) { 173 | expect(error).to.be.instanceof(Error); 174 | done(); 175 | }); 176 | }); 177 | 178 | }); 179 | 180 | describe('awaitCompletion', function () { 181 | var clock; 182 | var calledBack; 183 | 184 | beforeEach(function () { 185 | clock = sandbox.useFakeTimers(); 186 | calledBack = false; 187 | 188 | sandbox.stub(cloudFormationOperation, 'updateEventData').yields(); 189 | }); 190 | 191 | function run (type, setStatus, shouldError) { 192 | cloudFormationOperation.awaitCompletion( 193 | type, 194 | stackData, 195 | function (error) { 196 | if (shouldError) { 197 | expect(error).to.be.instanceof(Error); 198 | } 199 | else { 200 | expect(error).to.equal(undefined); 201 | } 202 | calledBack = true; 203 | } 204 | ); 205 | 206 | // It should loop the first time since the condition isn't satisfied. 207 | clock.tick(config.progressCheckIntervalInSeconds * 1000); 208 | expect(stackData.status).to.equal(undefined); 209 | sinon.assert.calledOnce(cloudFormationOperation.updateEventData); 210 | sinon.assert.calledWith( 211 | cloudFormationOperation.updateEventData, 212 | stackData, 213 | sinon.match.func 214 | ); 215 | expect(calledBack).to.equal(false); 216 | 217 | // Now set the result and that should result in completion. 218 | stackData.status = setStatus; 219 | clock.tick(config.progressCheckIntervalInSeconds * 1000); 220 | sinon.assert.calledTwice(cloudFormationOperation.updateEventData); 221 | sinon.assert.calledWith( 222 | cloudFormationOperation.updateEventData, 223 | stackData, 224 | sinon.match.func 225 | ); 226 | expect(calledBack).to.equal(true); 227 | } 228 | 229 | it('CREATE_STACK completes on resourceStatus.CREATE_COMPLETE', function () { 230 | config.onDeployFailure = constants.onDeployFailure.DELETE; 231 | run( 232 | constants.type.CREATE_STACK, 233 | constants.resourceStatus.CREATE_COMPLETE, 234 | false 235 | ); 236 | }); 237 | 238 | // Delete complete should produce an error even if successful to break out 239 | // of the flow of tasks and finish up. 240 | it('CREATE_STACK errors on resourceStatus.DELETE_COMPLETE', function () { 241 | config.onDeployFailure = constants.onDeployFailure.DELETE; 242 | run( 243 | constants.type.CREATE_STACK, 244 | constants.resourceStatus.DELETE_COMPLETE, 245 | true 246 | ); 247 | }); 248 | 249 | it('CREATE_STACK errors on resourceStatus.DELETE_FAILED', function () { 250 | config.onDeployFailure = constants.onDeployFailure.DELETE; 251 | run( 252 | constants.type.CREATE_STACK, 253 | constants.resourceStatus.DELETE_FAILED, 254 | true 255 | ); 256 | }); 257 | 258 | it('CREATE_STACK completes on resourceStatus.CREATE_FAILED when not deleting stack', function () { 259 | config.onDeployFailure = constants.onDeployFailure.DO_NOTHING; 260 | run( 261 | constants.type.CREATE_STACK, 262 | constants.resourceStatus.CREATE_COMPLETE, 263 | false 264 | ); 265 | }); 266 | 267 | it('CREATE_STACK errors on resourceStatus.CREATE_FAILED when not deleting stack', function () { 268 | config.onDeployFailure = constants.onDeployFailure.DO_NOTHING; 269 | run( 270 | constants.type.CREATE_STACK, 271 | constants.resourceStatus.CREATE_FAILED, 272 | true 273 | ); 274 | }); 275 | 276 | it('DELETE_STACK completes on resourceStatus.DELETE_COMPLETE', function () { 277 | run( 278 | constants.type.DELETE_STACK, 279 | constants.resourceStatus.DELETE_COMPLETE, 280 | false 281 | ); 282 | }); 283 | 284 | it('DELETE_STACK errors on resourceStatus.DELETE_FAILED', function () { 285 | run( 286 | constants.type.DELETE_STACK, 287 | constants.resourceStatus.DELETE_FAILED, 288 | true 289 | ); 290 | }); 291 | 292 | it('UPDATE_STACK completes on resourceStatus.UPDATE_COMPLETE', function () { 293 | run( 294 | constants.type.UPDATE_STACK, 295 | constants.resourceStatus.UPDATE_COMPLETE, 296 | false 297 | ); 298 | }); 299 | 300 | it('UPDATE_STACK errors on resourceStatus.UPDATE_ROLLBACK_COMPLETE', function () { 301 | run( 302 | constants.type.UPDATE_STACK, 303 | constants.resourceStatus.UPDATE_ROLLBACK_COMPLETE, 304 | true 305 | ); 306 | }); 307 | 308 | it('UPDATE_STACK errors on resourceStatus.UPDATE_ROLLBACK_FAILED', function () { 309 | run( 310 | constants.type.UPDATE_STACK, 311 | constants.resourceStatus.UPDATE_ROLLBACK_FAILED, 312 | true 313 | ); 314 | }); 315 | }); 316 | }); 317 | -------------------------------------------------------------------------------- /test/lib/configValidator.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Tests for the configuration validator. 3 | */ 4 | 5 | // Local. 6 | var configValidator = require('../../lib/configValidator'); 7 | var constants = require('../../lib/constants'); 8 | var resources = require('../resources'); 9 | 10 | describe('lib/configValidator', function () { 11 | var config; 12 | var errors; 13 | 14 | describe('deploy config', function () { 15 | 16 | function run (property, value, shouldError) { 17 | config = resources.getDeployConfig(); 18 | config[property] = value; 19 | errors = configValidator.validateDeployConfig(config); 20 | if (shouldError) { 21 | expect(errors.length).to.be.above(0); 22 | } 23 | else { 24 | expect(errors.length).to.equal(0); 25 | } 26 | } 27 | 28 | function shouldAccept (property, value) { 29 | run(property, value, false); 30 | } 31 | 32 | function shouldReject (property, value) { 33 | run(property, value, true); 34 | } 35 | 36 | it('validates correct configuration', function () { 37 | config = resources.getDeployConfig(); 38 | errors = configValidator.validateDeployConfig(config); 39 | expect(errors).to.eql([]); 40 | }); 41 | 42 | it('rejects invalid configurations, accepts valid configurations', function () { 43 | shouldReject('baseName', undefined); 44 | shouldReject('baseName', ''); 45 | 46 | shouldReject('version', undefined); 47 | shouldReject('version', ''); 48 | 49 | shouldReject('deployId', undefined); 50 | shouldReject('deployId', ''); 51 | shouldAccept('deployId', '7'); 52 | shouldAccept('deployId', 7); 53 | 54 | shouldReject('onDeployFailure', undefined); 55 | shouldReject('onDeployFailureFailure', ''); 56 | shouldAccept('onDeployFailure', constants.onDeployFailure.DELETE); 57 | shouldAccept('onDeployFailure', constants.onDeployFailure.DO_NOTHING); 58 | 59 | shouldReject('parameters', undefined); 60 | shouldReject('parameters', { numberIsInvalid: 7 }); 61 | shouldAccept('parameters', {}); 62 | shouldAccept('parameters', { arbitraryName: 'value' }); 63 | 64 | shouldReject('progressCheckIntervalInSeconds', undefined); 65 | shouldReject('progressCheckIntervalInSeconds', 0); 66 | shouldReject('progressCheckIntervalInSeconds', -1); 67 | shouldReject('progressCheckIntervalInSeconds', 'value'); 68 | 69 | shouldReject('onEventFn', undefined); 70 | shouldReject('onEventFn', 'value'); 71 | 72 | shouldReject('postCreationFn', undefined); 73 | shouldReject('postCreationFn', 'value'); 74 | 75 | shouldReject('priorInstance', undefined); 76 | shouldReject('priorInstance', 'value'); 77 | shouldAccept('priorInstance', constants.priorInstance.DELETE); 78 | shouldAccept('priorInstance', constants.priorInstance.DO_NOTHING); 79 | 80 | shouldReject('tags', undefined); 81 | shouldReject('tags', { numberIsInvalid: 7 }); 82 | shouldAccept('parameters', {}); 83 | shouldAccept('parameters', { arbitraryName: 'value' }); 84 | 85 | shouldReject('createStackTimeoutInMinutes', undefined); 86 | shouldReject('createStackTimeoutInMinutes', 'value'); 87 | shouldReject('createStackTimeoutInMinutes', -1); 88 | shouldAccept('createStackTimeoutInMinutes', 0); 89 | 90 | shouldReject('capabilities', ['x']); 91 | shouldAccept('capabilities', []); 92 | shouldAccept('capabilities', [constants.capabilities.CAPABILITY_IAM]); 93 | shouldAccept('capabilities', [constants.capabilities.CAPABILITY_NAMED_IAM]); 94 | shouldAccept('capabilities', [ 95 | constants.capabilities.CAPABILITY_IAM, 96 | constants.capabilities.CAPABILITY_NAMED_IAM 97 | ]); 98 | 99 | // The actually optional options property passed to AWS clients. 100 | shouldReject('clientOptions', 'value'); 101 | shouldAccept('clientOptions', {}); 102 | shouldAccept('clientOptions', undefined); 103 | 104 | // Adding extra unwanted property. 105 | shouldReject('x', 'value'); 106 | }); 107 | }); 108 | 109 | describe('preview update config', function () { 110 | 111 | function run (property, value, shouldError) { 112 | config = resources.getPreviewUpdateConfig(); 113 | config[property] = value; 114 | errors = configValidator.validatePreviewUpdateConfig(config); 115 | if (shouldError) { 116 | expect(errors.length).to.be.above(0); 117 | } 118 | else { 119 | expect(errors.length).to.equal(0); 120 | } 121 | } 122 | 123 | function shouldAccept (property, value) { 124 | run(property, value, false); 125 | } 126 | 127 | function shouldReject (property, value) { 128 | run(property, value, true); 129 | } 130 | 131 | it('validates correct configuration', function () { 132 | config = resources.getPreviewUpdateConfig(); 133 | errors = configValidator.validatePreviewUpdateConfig(config); 134 | expect(errors).to.eql([]); 135 | }); 136 | 137 | it('rejects invalid configurations, accepts valid configurations', function () { 138 | shouldReject('changeSetName', undefined); 139 | shouldReject('changeSetName', ''); 140 | 141 | shouldReject('deleteChangeSet', undefined); 142 | shouldReject('deleteChangeSet', {}); 143 | 144 | shouldReject('stackName', undefined); 145 | shouldReject('stackName', ''); 146 | 147 | shouldReject('parameters', undefined); 148 | shouldReject('parameters', { numberIsInvalid: 7 }); 149 | shouldAccept('parameters', {}); 150 | shouldAccept('parameters', { arbitraryName: 'value' }); 151 | 152 | shouldReject('tags', undefined); 153 | shouldReject('tags', { numberIsInvalid: 7 }); 154 | shouldAccept('parameters', {}); 155 | shouldAccept('parameters', { arbitraryName: 'value' }); 156 | 157 | shouldReject('progressCheckIntervalInSeconds', undefined); 158 | shouldReject('progressCheckIntervalInSeconds', 0); 159 | shouldReject('progressCheckIntervalInSeconds', -1); 160 | shouldReject('progressCheckIntervalInSeconds', 'value'); 161 | 162 | shouldReject('capabilities', ['x']); 163 | shouldAccept('capabilities', []); 164 | shouldAccept('capabilities', [constants.capabilities.CAPABILITY_IAM]); 165 | shouldAccept('capabilities', [constants.capabilities.CAPABILITY_NAMED_IAM]); 166 | shouldAccept('capabilities', [ 167 | constants.capabilities.CAPABILITY_IAM, 168 | constants.capabilities.CAPABILITY_NAMED_IAM 169 | ]); 170 | 171 | // The actually optional options property passed to AWS clients. 172 | shouldReject('clientOptions', 'value'); 173 | shouldAccept('clientOptions', {}); 174 | shouldAccept('clientOptions', undefined); 175 | 176 | // Adding extra unwanted property. 177 | shouldReject('x', 'value'); 178 | }); 179 | }); 180 | 181 | describe('update config', function () { 182 | 183 | function run (property, value, shouldError) { 184 | config = resources.getUpdateConfig(); 185 | config[property] = value; 186 | errors = configValidator.validateUpdateConfig(config); 187 | if (shouldError) { 188 | expect(errors.length).to.be.above(0); 189 | } 190 | else { 191 | expect(errors.length).to.equal(0); 192 | } 193 | } 194 | 195 | function shouldAccept (property, value) { 196 | run(property, value, false); 197 | } 198 | 199 | function shouldReject (property, value) { 200 | run(property, value, true); 201 | } 202 | 203 | it('validates correct configuration', function () { 204 | config = resources.getUpdateConfig(); 205 | errors = configValidator.validateUpdateConfig(config); 206 | expect(errors).to.eql([]); 207 | }); 208 | 209 | it('rejects invalid configurations, accepts valid configurations', function () { 210 | shouldReject('stackName', undefined); 211 | shouldReject('stackName', ''); 212 | 213 | shouldReject('parameters', undefined); 214 | shouldReject('parameters', { numberIsInvalid: 7 }); 215 | shouldAccept('parameters', {}); 216 | shouldAccept('parameters', { arbitraryName: 'value' }); 217 | 218 | shouldReject('progressCheckIntervalInSeconds', undefined); 219 | shouldReject('progressCheckIntervalInSeconds', 0); 220 | shouldReject('progressCheckIntervalInSeconds', -1); 221 | shouldReject('progressCheckIntervalInSeconds', 'value'); 222 | 223 | shouldReject('onEventFn', undefined); 224 | shouldReject('onEventFn', 'value'); 225 | 226 | shouldReject('tags', undefined); 227 | shouldReject('tags', { numberIsInvalid: 7 }); 228 | shouldAccept('parameters', {}); 229 | shouldAccept('parameters', { arbitraryName: 'value' }); 230 | 231 | shouldReject('capabilities', ['x']); 232 | shouldAccept('capabilities', []); 233 | shouldAccept('capabilities', [constants.capabilities.CAPABILITY_IAM]); 234 | shouldAccept('capabilities', [constants.capabilities.CAPABILITY_NAMED_IAM]); 235 | shouldAccept('capabilities', [ 236 | constants.capabilities.CAPABILITY_IAM, 237 | constants.capabilities.CAPABILITY_NAMED_IAM 238 | ]); 239 | 240 | // The actually optional options property passed to AWS clients. 241 | shouldReject('clientOptions', 'value'); 242 | shouldAccept('clientOptions', {}); 243 | shouldAccept('clientOptions', undefined); 244 | 245 | // Adding extra unwanted property. 246 | shouldReject('x', 'value'); 247 | }); 248 | }); 249 | }); 250 | -------------------------------------------------------------------------------- /test/lib/deploy.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Tests for the deploy code. 3 | */ 4 | 5 | // Local. 6 | var configValidator = require('../../lib/configValidator'); 7 | var constants = require('../../lib/constants'); 8 | var Deploy = require('../../lib/deploy'); 9 | var resources = require('../resources'); 10 | var utilities = require('../../lib/utilities'); 11 | 12 | describe('lib/deploy', function () { 13 | var config; 14 | var template; 15 | var sandbox; 16 | var result; 17 | var stackId; 18 | 19 | beforeEach(function () { 20 | sandbox = sinon.sandbox.create(); 21 | 22 | config = resources.getDeployConfig(); 23 | template = JSON.stringify({}); 24 | deploy = new Deploy(config, template); 25 | stackId = 'id'; 26 | result = { 27 | timedOut: false, 28 | errors: [], 29 | createStack: deploy.getStackData(utilities.determineStackName(config)), 30 | describeStack: undefined, 31 | deleteStack: [] 32 | }; 33 | 34 | // Stub the event callback. 35 | sandbox.stub(config, 'onEventFn').returns(); 36 | 37 | // Make sure we stub everything that is used. 38 | sandbox.stub(deploy.cloudFormation, 'createStack').yields(null, { 39 | StackId: stackId 40 | }); 41 | sandbox.stub(deploy.cloudFormation, 'deleteStack').yields(); 42 | sandbox.stub(deploy.cloudFormation, 'describeStack').yields(null, { 43 | StackId: stackId 44 | }); 45 | sandbox.stub(deploy.cloudFormation, 'describePriorStacks').yields(null, []); 46 | sandbox.stub(deploy.cloudFormation, 'describeStackEvents').yields(null, []); 47 | sandbox.stub(deploy.cloudFormation, 'validateTemplate').yields(); 48 | }); 49 | 50 | afterEach(function () { 51 | sandbox.restore(); 52 | }); 53 | 54 | describe('deleteStack', function () { 55 | it('invokes underlying functions', function (done) { 56 | sandbox.stub(deploy, 'awaitCompletion').yields(); 57 | 58 | result.createStack.stackId = stackId; 59 | 60 | deploy.deleteStack(result.createStack, function (error) { 61 | sinon.assert.callOrder( 62 | deploy.cloudFormation.deleteStack, 63 | deploy.awaitCompletion 64 | ); 65 | sinon.assert.calledWith( 66 | deploy.cloudFormation.deleteStack, 67 | result.createStack.stackId, 68 | sinon.match.func 69 | ); 70 | sinon.assert.calledWith( 71 | deploy.awaitCompletion, 72 | constants.type.DELETE_STACK, 73 | result.createStack, 74 | sinon.match.func 75 | ); 76 | 77 | done(error); 78 | }); 79 | 80 | }); 81 | }); 82 | 83 | describe('deletePriorStacks', function () { 84 | beforeEach(function () { 85 | sandbox.stub(deploy, 'deleteStack').yields(); 86 | }); 87 | 88 | it('deletes only matching stacks', function (done) { 89 | var matchingStackName = config.baseName + '-' + 'dummy'; 90 | var matchingStackId = 'a-valid-stack-id'; 91 | result.createStack.stackId = 'created-stack-id'; 92 | 93 | deploy.cloudFormation.describePriorStacks.yields(null, [ 94 | { 95 | StackName: matchingStackName, 96 | StackId: matchingStackId 97 | } 98 | ]); 99 | 100 | deploy.deletePriorStacks(result, function (error) { 101 | sinon.assert.calledWith( 102 | deploy.cloudFormation.describePriorStacks, 103 | config.baseName, 104 | result.createStack.stackId, 105 | sinon.match.func 106 | ); 107 | sinon.assert.calledOnce(deploy.deleteStack); 108 | sinon.assert.calledWith( 109 | deploy.deleteStack, 110 | deploy.getStackData( 111 | matchingStackName, 112 | matchingStackId 113 | ), 114 | sinon.match.func 115 | ); 116 | 117 | done(error); 118 | }); 119 | }); 120 | }); 121 | 122 | describe('deploy', function () { 123 | 124 | beforeEach(function () { 125 | sandbox.stub(configValidator, 'validate').returns([]); 126 | sandbox.stub(deploy, 'awaitCompletion').yields(); 127 | sandbox.stub(config, 'postCreationFn').yields(); 128 | sandbox.stub(deploy, 'deletePriorStacks').yields(); 129 | }); 130 | 131 | it('invokes underlying functions when no errors are created', function (done) { 132 | deploy.deploy(function (error, generatedResult) { 133 | sinon.assert.callOrder( 134 | configValidator.validate, 135 | deploy.cloudFormation.validateTemplate, 136 | deploy.cloudFormation.createStack, 137 | deploy.awaitCompletion, 138 | deploy.cloudFormation.describeStack, 139 | config.postCreationFn, 140 | deploy.deletePriorStacks 141 | ); 142 | 143 | sinon.assert.calledWith( 144 | configValidator.validate, 145 | config 146 | ); 147 | sinon.assert.calledWith( 148 | deploy.cloudFormation.validateTemplate, 149 | template, 150 | sinon.match.func 151 | ); 152 | sinon.assert.calledWith( 153 | deploy.cloudFormation.createStack, 154 | template, 155 | sinon.match.func 156 | ); 157 | sinon.assert.calledWith( 158 | deploy.awaitCompletion, 159 | constants.type.CREATE_STACK, 160 | deploy.getStackData( 161 | utilities.determineStackName(config), 162 | stackId 163 | ), 164 | sinon.match.func 165 | ); 166 | sinon.assert.calledWith( 167 | deploy.cloudFormation.describeStack, 168 | stackId, 169 | sinon.match.func 170 | ); 171 | sinon.assert.calledWith( 172 | config.postCreationFn, 173 | generatedResult.describeStack, 174 | sinon.match.func 175 | ); 176 | sinon.assert.calledWith( 177 | deploy.deletePriorStacks, 178 | generatedResult, 179 | sinon.match.func 180 | ); 181 | 182 | expect(generatedResult).to.be.instanceof(Object); 183 | expect(generatedResult.createStack.stackId).to.equal(stackId); 184 | 185 | done(error); 186 | }); 187 | }); 188 | }); 189 | 190 | }); 191 | -------------------------------------------------------------------------------- /test/lib/previewUpdate.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Tests for the update preview code. 3 | */ 4 | 5 | // Local. 6 | var configValidator = require('../../lib/configValidator'); 7 | var constants = require('../../lib/constants'); 8 | var resources = require('../resources'); 9 | var PreviewUpdate = require('../../lib/previewUpdate'); 10 | 11 | describe('lib/deploy', function () { 12 | var completeChangeSet; 13 | var config; 14 | var incompleteChangeSet; 15 | var template; 16 | var sandbox; 17 | var previewUpdate; 18 | var result; 19 | 20 | beforeEach(function () { 21 | sandbox = sinon.sandbox.create(); 22 | 23 | config = resources.getPreviewUpdateConfig(); 24 | template = JSON.stringify({}); 25 | previewUpdate = new PreviewUpdate(config, template); 26 | 27 | 28 | completeChangeSet = { 29 | Status: constants.changeSetStatus.CREATE_COMPLETE 30 | }; 31 | incompleteChangeSet = { 32 | Status: '' 33 | }; 34 | 35 | result = { 36 | errors: [], 37 | changeSet: completeChangeSet 38 | }; 39 | 40 | // Make sure we stub everything that is used. 41 | sandbox.stub(previewUpdate.cloudFormation, 'createChangeSet').yields(); 42 | sandbox.stub(previewUpdate.cloudFormation, 'deleteChangeSet').yields(); 43 | sandbox.stub(previewUpdate.cloudFormation, 'describeChangeSet').yields(); 44 | sandbox.stub(previewUpdate.cloudFormation, 'validateTemplate').yields(); 45 | 46 | previewUpdate.cloudFormation.describeChangeSet.onCall(0).yields( 47 | null, 48 | incompleteChangeSet 49 | ); 50 | previewUpdate.cloudFormation.describeChangeSet.onCall(1).yields( 51 | null, 52 | completeChangeSet 53 | ); 54 | }); 55 | 56 | afterEach(function () { 57 | sandbox.restore(); 58 | }); 59 | 60 | describe('awaitCompletion', function () { 61 | var clock; 62 | var calledBack; 63 | 64 | beforeEach(function () { 65 | clock = sandbox.useFakeTimers(); 66 | calledBack = false; 67 | }); 68 | 69 | function run (setStatus) { 70 | previewUpdate.awaitCompletion(function (error, changeSet) { 71 | calledBack = true; 72 | expect(error).to.equal(null); 73 | expect(changeSet).to.eql(completeChangeSet); 74 | }); 75 | 76 | // It should loop the first time since the condition isn't satisfied. 77 | clock.tick(config.progressCheckIntervalInSeconds * 1000); 78 | sinon.assert.calledOnce(previewUpdate.cloudFormation.describeChangeSet); 79 | sinon.assert.calledWith( 80 | previewUpdate.cloudFormation.describeChangeSet, 81 | sinon.match.func 82 | ); 83 | expect(calledBack).to.equal(false); 84 | 85 | // Now it should result in completion. 86 | clock.tick(config.progressCheckIntervalInSeconds * 1000); 87 | sinon.assert.calledTwice(previewUpdate.cloudFormation.describeChangeSet); 88 | sinon.assert.calledWith( 89 | previewUpdate.cloudFormation.describeChangeSet, 90 | sinon.match.func 91 | ); 92 | expect(calledBack).to.equal(true); 93 | } 94 | 95 | it('completes on CREATE_COMPLETE', function () { 96 | run(constants.changeSetStatus.CREATE_COMPLETE); 97 | }); 98 | 99 | it('completes on FAILED', function () { 100 | run(constants.changeSetStatus.FAILED); 101 | }); 102 | }); 103 | 104 | describe('previewUpdate', function () { 105 | 106 | beforeEach(function () { 107 | sandbox.stub(configValidator, 'validate').returns([]); 108 | sandbox.stub(previewUpdate, 'awaitCompletion').yields( 109 | null, 110 | completeChangeSet 111 | ); 112 | }); 113 | 114 | it('invokes underlying functions when no errors are created', function (done) { 115 | previewUpdate.previewUpdate(function (error, generatedResult) { 116 | sinon.assert.callOrder( 117 | configValidator.validate, 118 | previewUpdate.cloudFormation.validateTemplate, 119 | previewUpdate.cloudFormation.createChangeSet, 120 | previewUpdate.awaitCompletion, 121 | previewUpdate.cloudFormation.deleteChangeSet 122 | ); 123 | 124 | sinon.assert.calledWith( 125 | configValidator.validate, 126 | config 127 | ); 128 | sinon.assert.calledWith( 129 | previewUpdate.cloudFormation.validateTemplate, 130 | template, 131 | sinon.match.func 132 | ); 133 | sinon.assert.calledWith( 134 | previewUpdate.cloudFormation.createChangeSet, 135 | template, 136 | sinon.match.func 137 | ); 138 | sinon.assert.calledWith( 139 | previewUpdate.awaitCompletion, 140 | sinon.match.func 141 | ); 142 | sinon.assert.calledWith( 143 | previewUpdate.cloudFormation.deleteChangeSet, 144 | sinon.match.func 145 | ); 146 | 147 | expect(generatedResult).to.eql(result); 148 | 149 | done(error); 150 | }); 151 | }); 152 | }); 153 | 154 | }); 155 | -------------------------------------------------------------------------------- /test/lib/update.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Tests for the update code. 3 | */ 4 | 5 | // Local. 6 | var configValidator = require('../../lib/configValidator'); 7 | var constants = require('../../lib/constants'); 8 | var resources = require('../resources'); 9 | var Update = require('../../lib/update'); 10 | var utilities = require('../../lib/utilities'); 11 | 12 | describe('lib/deploy', function () { 13 | var config; 14 | var template; 15 | var sandbox; 16 | var stackId; 17 | var update; 18 | 19 | beforeEach(function () { 20 | sandbox = sinon.sandbox.create(); 21 | 22 | config = resources.getUpdateConfig(); 23 | template = JSON.stringify({}); 24 | update = new Update(config, template); 25 | stackId = 'id'; 26 | 27 | // Stub the event callback. 28 | sandbox.stub(config, 'onEventFn').returns(); 29 | 30 | // Make sure we stub everything that is used. 31 | sandbox.stub(update.cloudFormation, 'updateStack').yields(null, { 32 | StackId: stackId 33 | }); 34 | sandbox.stub(update.cloudFormation, 'describeStack').yields(null, { 35 | StackId: stackId 36 | }); 37 | sandbox.stub(update.cloudFormation, 'describeStackEvents').yields(null, []); 38 | sandbox.stub(update.cloudFormation, 'validateTemplate').yields(); 39 | }); 40 | 41 | afterEach(function () { 42 | sandbox.restore(); 43 | }); 44 | 45 | describe('update', function () { 46 | 47 | beforeEach(function () { 48 | sandbox.stub(configValidator, 'validate').returns([]); 49 | sandbox.stub(update, 'awaitCompletion').yields(); 50 | }); 51 | 52 | it('invokes underlying functions when no errors are created', function (done) { 53 | update.update(function (error, generatedResult) { 54 | sinon.assert.callOrder( 55 | configValidator.validate, 56 | update.cloudFormation.validateTemplate, 57 | update.cloudFormation.updateStack, 58 | update.awaitCompletion, 59 | update.cloudFormation.describeStack 60 | ); 61 | 62 | sinon.assert.calledWith( 63 | configValidator.validate, 64 | config 65 | ); 66 | sinon.assert.calledWith( 67 | update.cloudFormation.validateTemplate, 68 | template, 69 | sinon.match.func 70 | ); 71 | sinon.assert.calledWith( 72 | update.cloudFormation.updateStack, 73 | template, 74 | sinon.match.func 75 | ); 76 | sinon.assert.calledWith( 77 | update.awaitCompletion, 78 | constants.type.UPDATE_STACK, 79 | update.getStackData( 80 | utilities.determineStackName(config), 81 | stackId 82 | ), 83 | sinon.match.func 84 | ); 85 | sinon.assert.calledWith( 86 | update.cloudFormation.describeStack, 87 | stackId, 88 | sinon.match.func 89 | ); 90 | 91 | expect(generatedResult).to.be.instanceof(Object); 92 | expect(generatedResult.updateStack.stackId).to.equal(stackId); 93 | 94 | done(error); 95 | }); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/lib/utilities.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Tests for shared utilities. 3 | */ 4 | 5 | // NPM. 6 | var _ = require('lodash'); 7 | 8 | // Local. 9 | var constants = require('../../lib/constants'); 10 | var resources = require('../resources'); 11 | var utilities = require('../../lib/utilities'); 12 | 13 | describe('lib/utilities', function () { 14 | 15 | var deployConfig; 16 | var updateConfig; 17 | 18 | beforeEach(function () { 19 | deployConfig = resources.getDeployConfig(); 20 | updateConfig = resources.getUpdateConfig(); 21 | }); 22 | 23 | describe('getStackName', function () { 24 | it('functions correctly for deploy config', function () { 25 | expect(utilities.determineStackName(deployConfig)).to.equal( 26 | deployConfig.baseName + '-' + deployConfig.deployId 27 | ); 28 | }); 29 | 30 | it('functions correctly for update config', function () { 31 | expect(utilities.determineStackName(updateConfig)).to.equal( 32 | updateConfig.stackName 33 | ); 34 | }); 35 | 36 | it('replaces invalid characters with dashes', function () { 37 | deployConfig.baseName = '._%a'; 38 | deployConfig.deployId = '._%b'; 39 | 40 | expect(utilities.determineStackName(deployConfig)).to.equal('---a----b'); 41 | }); 42 | }); 43 | 44 | describe('baseNameMatchesStackName', function () { 45 | it('functions correctly', function () { 46 | expect(utilities.baseNameMatchesStackName('a', 'a-b')).to.equal(true); 47 | expect(utilities.baseNameMatchesStackName('a', 'aa-b')).to.equal(false); 48 | }); 49 | }); 50 | 51 | describe('getParameters', function () { 52 | it('functions correctly', function () { 53 | expect(utilities.getParameters(deployConfig)).to.eql([ 54 | { 55 | ParameterKey: 'name', 56 | ParameterValue: 'value' 57 | } 58 | ]); 59 | }); 60 | 61 | it('functions correctly for missing parameters', function () { 62 | deployConfig.parameters = undefined; 63 | expect(utilities.getParameters(deployConfig)).to.eql([]); 64 | }); 65 | }); 66 | 67 | describe('getTags', function () { 68 | it('functions correctly for deploy config', function () { 69 | expect(utilities.getTags(deployConfig)).to.eql([ 70 | { 71 | Key: 'a', 72 | Value: 'b' 73 | }, 74 | { 75 | Key: constants.tag.STACK_NAME, 76 | Value: 'test-1' 77 | }, 78 | { 79 | Key: constants.tag.STACK_BASE_NAME, 80 | Value: 'test' 81 | }, 82 | { 83 | Key: constants.tag.VERSION, 84 | Value: '1.0.0' 85 | } 86 | ]); 87 | }); 88 | 89 | it('functions correctly for update config', function () { 90 | expect(utilities.getTags(updateConfig)).to.eql([ 91 | { 92 | Key: 'a', 93 | Value: 'b' 94 | }, 95 | { 96 | Key: constants.tag.STACK_NAME, 97 | Value: 'test' 98 | } 99 | ]); 100 | }); 101 | 102 | it('functions correctly for missing tags', function () { 103 | deployConfig.tags = undefined; 104 | expect(utilities.getTags(deployConfig)).to.eql([ 105 | { 106 | Key: constants.tag.STACK_NAME, 107 | Value: 'test-1' 108 | }, 109 | { 110 | Key: constants.tag.STACK_BASE_NAME, 111 | Value: 'test' 112 | }, 113 | { 114 | Key: constants.tag.VERSION, 115 | Value: '1.0.0' 116 | } 117 | ]); 118 | }); 119 | }); 120 | 121 | describe('addTemplatePropertyToParameters', function () { 122 | var params; 123 | var template; 124 | 125 | beforeEach(function () { 126 | params = {}; 127 | }); 128 | 129 | it('adds TemplateURL for URL', function () { 130 | template = 'http://s3.amazonaws.com/bucket/example.json'; 131 | utilities.addTemplatePropertyToParameters(params, template); 132 | 133 | expect(params).to.eql({ 134 | TemplateURL: template 135 | }); 136 | }); 137 | 138 | it('adds TemplateBody for other string', function () { 139 | template = JSON.stringify({}); 140 | utilities.addTemplatePropertyToParameters(params, template); 141 | 142 | expect(params).to.eql({ 143 | TemplateBody: template 144 | }); 145 | }); 146 | 147 | it('adds TemplateBody for object', function () { 148 | template = {}; 149 | utilities.addTemplatePropertyToParameters(params, template); 150 | 151 | expect(params).to.eql({ 152 | TemplateBody: JSON.stringify(template) 153 | }); 154 | }); 155 | }); 156 | 157 | describe('fillDeployConfigurationDefaults', function () { 158 | it('fills defaults', function () { 159 | var deployConfigA = utilities.fillDeployConfigurationDefaults({ 160 | baseName: 'test', 161 | version: '1.0.0', 162 | deployId: '1' 163 | }); 164 | var deployConfigB = { 165 | clientOptions: undefined, 166 | capabilities: _.values(constants.capabilities), 167 | baseName: 'test', 168 | version: '1.0.0', 169 | deployId: '1', 170 | tags: {}, 171 | parameters: {}, 172 | progressCheckIntervalInSeconds: 10, 173 | onEventFn: function () {}, 174 | postCreationFn: function (stackDescription, callback) { 175 | callback(); 176 | }, 177 | createStackTimeoutInMinutes: 10, 178 | priorInstance: constants.priorInstance.DELETE, 179 | onDeployFailure: constants.onDeployFailure.DELETE 180 | }; 181 | 182 | // Comparing functions, have to take account of different indentations. 183 | deployConfigA.onEventFn = deployConfigA.onEventFn.toString().replace(/\s+/g, ' '); 184 | deployConfigB.onEventFn = deployConfigB.onEventFn.toString().replace(/\s+/g, ' '); 185 | deployConfigA.postCreationFn = deployConfigA.postCreationFn.toString().replace(/\s+/g, ' '); 186 | deployConfigB.postCreationFn = deployConfigB.postCreationFn.toString().replace(/\s+/g, ' '); 187 | 188 | expect(deployConfigA).to.eql(deployConfigB); 189 | }); 190 | 191 | it('does not override values', function () { 192 | deployConfig = { 193 | clientOptions: undefined, 194 | capabilities: _.values(constants.capabilities), 195 | baseName: 'test', 196 | version: '1.0.0', 197 | deployId: '1', 198 | tags: { 199 | x: 'y' 200 | }, 201 | parameters: { 202 | alpha: 'beta' 203 | }, 204 | progressCheckIntervalInSeconds: 15, 205 | onEventFn: function (event) { 206 | return JSON.stringify(event); 207 | }, 208 | postCreationFn: function (stackDescription, callback) { 209 | callback(new Error()); 210 | }, 211 | createStackTimeoutInMinutes: 5, 212 | priorInstance: constants.priorInstance.DO_NOTHING, 213 | onDeployFailure: constants.onDeployFailure.DO_NOTHING 214 | }; 215 | 216 | expect(utilities.fillDeployConfigurationDefaults(deployConfig)).to.eql(deployConfig); 217 | }); 218 | }); 219 | 220 | describe('fillPreviewUpdateConfigurationDefaults', function () { 221 | it('fills defaults', function () { 222 | var updateConfigA = utilities.fillPreviewUpdateConfigurationDefaults({ 223 | changeSetName: 'testChangeSet', 224 | stackName: 'test' 225 | }); 226 | var updateConfigB = { 227 | changeSetName: 'testChangeSet', 228 | clientOptions: undefined, 229 | capabilities: _.values(constants.capabilities), 230 | deleteChangeSet: true, 231 | stackName: 'test', 232 | tags: {}, 233 | parameters: {}, 234 | progressCheckIntervalInSeconds: 10 235 | }; 236 | 237 | expect(updateConfigA).to.eql(updateConfigB); 238 | }); 239 | 240 | it('does not override values', function () { 241 | updateConfig = { 242 | changeSetName: 'testChangeSet', 243 | clientOptions: undefined, 244 | capabilities: _.values(constants.capabilities), 245 | deleteChangeSet: false, 246 | stackName: 'test', 247 | tags: { 248 | x: 'y' 249 | }, 250 | parameters: { 251 | alpha: 'beta' 252 | }, 253 | progressCheckIntervalInSeconds: 15 254 | }; 255 | 256 | expect(utilities.fillPreviewUpdateConfigurationDefaults(updateConfig)).to.eql(updateConfig); 257 | }); 258 | }); 259 | 260 | describe('fillUpdateConfigurationDefaults', function () { 261 | it('fills defaults', function () { 262 | var updateConfigA = utilities.fillUpdateConfigurationDefaults({ 263 | stackName: 'test' 264 | }); 265 | var updateConfigB = { 266 | clientOptions: undefined, 267 | capabilities: _.values(constants.capabilities), 268 | stackName: 'test', 269 | tags: {}, 270 | parameters: {}, 271 | progressCheckIntervalInSeconds: 10, 272 | onEventFn: function () {} 273 | }; 274 | 275 | // Comparing functions, have to take account of different indentations. 276 | updateConfigA.onEventFn = updateConfigA.onEventFn.toString().replace(/\s+/g, ' '); 277 | updateConfigB.onEventFn = updateConfigB.onEventFn.toString().replace(/\s+/g, ' '); 278 | 279 | expect(updateConfigA).to.eql(updateConfigB); 280 | }); 281 | 282 | it('does not override values', function () { 283 | updateConfig = { 284 | clientOptions: undefined, 285 | capabilities: _.values(constants.capabilities), 286 | stackName: 'test', 287 | tags: { 288 | x: 'y' 289 | }, 290 | parameters: { 291 | alpha: 'beta' 292 | }, 293 | progressCheckIntervalInSeconds: 15, 294 | onEventFn: function (event) { 295 | return JSON.stringify(event); 296 | } 297 | }; 298 | 299 | expect(utilities.fillUpdateConfigurationDefaults(updateConfig)).to.eql(updateConfig); 300 | }); 301 | }); 302 | 303 | }); 304 | -------------------------------------------------------------------------------- /test/mochaInit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Set up some of the globals for testing. 3 | */ 4 | 5 | var chai = require('chai'); 6 | var sinon = require('sinon'); 7 | 8 | global.sinon = sinon; 9 | 10 | global.assert = chai.assert; 11 | global.expect = chai.expect; 12 | global.should = chai.should(); 13 | -------------------------------------------------------------------------------- /test/resources/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Resources for testing. 3 | */ 4 | 5 | // NPM. 6 | var _ = require('lodash'); 7 | 8 | // Local. 9 | var utilities = require('../../lib/utilities'); 10 | 11 | /** 12 | * Return a deploy configuration object, overwriting its defaults with the 13 | * provided values. 14 | * 15 | * @param {Object} config Optional partial configuration with override values. 16 | * @return {Object} Configuration object. 17 | */ 18 | exports.getDeployConfig = function (config) { 19 | return utilities.fillDeployConfigurationDefaults(_.extend({ 20 | clientOptions: undefined, 21 | baseName: 'test', 22 | version: '1.0.0', 23 | deployId: '1', 24 | tags: { 25 | a: 'b' 26 | }, 27 | parameters: { 28 | name: 'value' 29 | } 30 | }, config)); 31 | }; 32 | 33 | /** 34 | * Return an update configuration object, overwriting its defaults with the 35 | * provided values. 36 | * 37 | * @param {Object} config Optional partial configuration with override values. 38 | * @return {Object} Configuration object. 39 | */ 40 | exports.getPreviewUpdateConfig = function (config) { 41 | return utilities.fillPreviewUpdateConfigurationDefaults(_.extend({ 42 | changeSetName: 'testChangeSet', 43 | clientOptions: undefined, 44 | stackName: 'test', 45 | tags: { 46 | a: 'b' 47 | }, 48 | parameters: { 49 | name: 'value' 50 | } 51 | }, config)); 52 | }; 53 | 54 | /** 55 | * Return an update configuration object, overwriting its defaults with the 56 | * provided values. 57 | * 58 | * @param {Object} config Optional partial configuration with override values. 59 | * @return {Object} Configuration object. 60 | */ 61 | exports.getUpdateConfig = function (config) { 62 | return utilities.fillUpdateConfigurationDefaults(_.extend({ 63 | clientOptions: undefined, 64 | stackName: 'test', 65 | tags: { 66 | a: 'b' 67 | }, 68 | parameters: { 69 | name: 'value' 70 | } 71 | }, config)); 72 | }; 73 | --------------------------------------------------------------------------------