├── .gitignore ├── .ruby-gemset ├── .ruby-version ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin └── simple_deploy ├── lib ├── simple_deploy.rb └── simple_deploy │ ├── artifact.rb │ ├── aws.rb │ ├── aws │ ├── cloud_formation.rb │ ├── cloud_formation │ │ └── error.rb │ ├── helpers.rb │ ├── instance_reader.rb │ └── simpledb.rb │ ├── backoff.rb │ ├── cli.rb │ ├── cli │ ├── attributes.rb │ ├── clone.rb │ ├── create.rb │ ├── deploy.rb │ ├── destroy.rb │ ├── environments.rb │ ├── events.rb │ ├── execute.rb │ ├── instances.rb │ ├── list.rb │ ├── outputs.rb │ ├── parameters.rb │ ├── protect.rb │ ├── resources.rb │ ├── shared.rb │ ├── status.rb │ ├── template.rb │ └── update.rb │ ├── configuration.rb │ ├── entry.rb │ ├── entry_lister.rb │ ├── env.rb │ ├── exceptions.rb │ ├── logger.rb │ ├── misc.rb │ ├── misc │ └── attribute_merger.rb │ ├── notifier.rb │ ├── notifier │ ├── campfire.rb │ └── slack.rb │ ├── stack.rb │ ├── stack │ ├── deployment.rb │ ├── deployment │ │ └── status.rb │ ├── execute.rb │ ├── output_mapper.rb │ ├── ssh.rb │ ├── stack_attribute_formatter.rb │ ├── stack_creator.rb │ ├── stack_destroyer.rb │ ├── stack_formatter.rb │ ├── stack_lister.rb │ ├── stack_reader.rb │ ├── stack_updater.rb │ └── status.rb │ ├── template.rb │ └── version.rb ├── simple_deploy.gemspec └── spec ├── artifact_spec.rb ├── aws ├── cloud_formation │ └── error_spec.rb ├── cloud_formation_spec.rb ├── helpers_spec.rb ├── instance_reader_spec.rb └── simpledb_spec.rb ├── backoff_spec.rb ├── cli ├── attributes_spec.rb ├── clone_spec.rb ├── create_spec.rb ├── deploy_spec.rb ├── destroy_spec.rb ├── outputs_spec.rb ├── protect_spec.rb ├── shared_spec.rb └── update_spec.rb ├── cli_spec.rb ├── config_spec.rb ├── contexts ├── config_contexts.rb ├── logger_contexts.rb └── stack_contexts.rb ├── entry_lister_spec.rb ├── entry_spec.rb ├── logger_spec.rb ├── misc └── attribute_merger_spec.rb ├── notifier ├── campfire_spec.rb └── slack_spec.rb ├── notifier_spec.rb ├── spec_helper.rb ├── stack ├── deployment │ └── status_spec.rb ├── deployment_spec.rb ├── execute_spec.rb ├── output_mapper_spec.rb ├── ssh_spec.rb ├── stack_attribute_formatter_spec.rb ├── stack_creator_spec.rb ├── stack_destroyer_spec.rb ├── stack_formatter_spec.rb ├── stack_lister_spec.rb ├── stack_reader_spec.rb ├── stack_updater_spec.rb └── status_spec.rb ├── stack_spec.rb └── template_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | coverage 6 | *.swp 7 | .idea/* 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | simple_deploy 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.0.0-p247 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.2 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1.0 6 | script: "rake spec" 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## HEAD 2 | 3 | ## v0.10.2 (05/07/15) 4 | * Add support for sending notifications to Slack 5 | 6 | ## v0.10.0 (08/27/14): 7 | 8 | * Add support for setting configuration through env vars 9 | * Add support for temporary AWS credentials 10 | 11 | ## v0.9.2 (04/29/14): 12 | 13 | * Skip nested stack if status is deleted (thanks @liwenbing) 14 | 15 | ## v0.9.1 (04/24/14): 16 | 17 | * Pinning to fog-core 1.21.1 18 | 19 | ## v0.9.0 (04/24/14): 20 | 21 | * Adding support for pty to execute 22 | 23 | ## v0.8.2 (04/02/14): 24 | 25 | * Support instances in nested stacks (thanks @liwenbing) 26 | * Support instances created by instance resource (thanks @liwenbing) 27 | 28 | ## v0.8.1 (03/25/14): 29 | 30 | * Update fog to 1.21 31 | * Instances and Deploy support for multi ASGs in stack 32 | 33 | ## v0.8.0 (12/13/13): 34 | 35 | * Added flag to specify external ips 36 | 37 | ## v0.7.8 (12/11/13): 38 | 39 | * Added runtime dependency for unf and unf_ext for fog 40 | 41 | ## v0.7.7 (12/6/13): 42 | 43 | * Update to fog 1.18 44 | * Update to excon 0.28 45 | 46 | ## v0.7.6 (10/30/13): 47 | 48 | * Updated to bind to excon 0.25.3 49 | * Updated to latest fog 1.15 50 | * Added feature for issue 169 to remove values which are set to nil 51 | * Added feature for issue 191 to read in input stack for clone command 52 | * Fixed issue 176 where multiple deploys did not exit if first deploy failed 53 | * Fixed find_xml_file error when a blank exception is thrown from cloudformation. 54 | * Add 'envs' command as a shortcut for 'environments' 55 | 56 | ## v0.7.5 (07/02/13): 57 | 58 | * Fixed bug with forced deployment in 0.7.4 59 | 60 | ## v0.7.4: 61 | 62 | * Updated the update command so it can now take a new template 63 | 64 | ## v0.7.3 (05/17/2013): 65 | 66 | * Merged the simple_deploy and stackster gems 67 | * Replaced the tinder gem with the esbit gem for campfire communications 68 | * Refactored the combined source code to make it easier to maintain 69 | 70 | ## v0.7.2 (03/20/2013): 71 | 72 | * Fix issue when trying to use ssh command (fixes #172) 73 | * Upgrade to stackster 0.4.4 to address issue with getting instances 74 | * Modified backoff to help prevent exceeding API limits 75 | 76 | ## v0.7.1: 77 | 78 | * Updated template download logic in clone to fix issu #160 79 | 80 | ## v0.7.0: 81 | 82 | * Added --as-command-args argument to outputs command 83 | * Added support to read stack parameters from other stack outputs 84 | * Better handling of errors for deploy/execute commands (#149, #150, #155) 85 | 86 | ## v0.6.7: 87 | 88 | * Upgraded to stackster 0.4.2 89 | 90 | ## v0.6.6: 91 | 92 | * Downreving tinder to play nice w/ berkshelf 93 | 94 | ## v0.6.5: 95 | 96 | * Fixed instances command handling of nil IP addresses 97 | * Upgraded to stackster 0.4.1 98 | * Remove 'ssh' command 99 | * Update output format for 'attributes', 'instances', 'parameters' commands 100 | * Update help output 101 | * Minor internal cli refactor 102 | * Removed unused env argument passed into Stackster::Stack 103 | * Added log_level to clone cli class 104 | * Catching stackster exceptions in CLI 105 | * Refactored shared specs 106 | 107 | ## v0.6.4: 108 | 109 | * Check for artifact encryption via attribute name_encrypted (where name is name of archive) 110 | * Appendes .gpg to URL for encrypted archives 111 | 112 | ## v0.6.3: 113 | 114 | * Instances return different message when no instances vs non existent stack 115 | * Updating rvm to ruby 1.9.3-p327 116 | * Updated to stackster version 0.4.0 117 | 118 | ## v0.6.2 119 | 120 | * Corrected the deploy command so it exits if a stack update fails 121 | * Fixed help so it shows all commands 122 | * Refactored the outputs command to show less whitespace 123 | 124 | ## v0.6.1 125 | 126 | * Corrected the clone command so it accepts new attributes 127 | * Added ability to pass config data into Config class which avoids having to have a .simple_deploy.yml file. 128 | * Config file reads SIMPLE_DEPLOY_CONFIG_FILE for alternate config file location. 129 | 130 | ## v0.6.0 131 | 132 | * Updated so it no longer caches simpledb attributes so it works better with stackster changes 133 | * Upgraded to stackster 0.3.2 134 | * Added execute subcommand to run arbitray commands against stack 135 | * Refactored deployment to use ssh library 136 | * Added specs 137 | 138 | ## v0.5.6 139 | 140 | * Corrected the instances command so it returns all instances 141 | * Updated the clone command so it can take a new template for the new stack 142 | * Added region to the message notifications 143 | * Fixed deployments so they don't try to deploy if the attributes fail to update 144 | 145 | ## v0.5.5 146 | 147 | * Added backoff functionality to updates so they work better with the force option 148 | * Now publishes a message when the deployment starts 149 | 150 | ## v0.5.4 151 | 152 | * Added support for internal networking to the deploy and instances commands 153 | * Added backoff functionality to deployments so they work better with the force option 154 | 155 | ## v0.5.3 156 | 157 | * Added a clone command that clones a stack 158 | * Updated the protect command so it allows multiple stacks 159 | * Updated the instances command so it tells the user when the stack does not exist 160 | * Updated the outputs command to make the output values easier to read 161 | 162 | ## v0.5.2 163 | 164 | * Upgrade to stackster 0.3.0 165 | * Corrected handling of attributes on stack creation 166 | 167 | ## v0.5.1: 168 | 169 | * Corrected the use of the domain attribute in URLs during deployments 170 | * Corrected the use of the force option during deployments 171 | * Added help text on SSH usage to the deploy command 172 | * Corrected the acceptable options for the protect command 173 | 174 | ## v0.5.0: 175 | 176 | * Updated the update command so it accepts a force option 177 | * Refactored deployments to read bucket_prefix and domain from attributes instead of the yaml file 178 | * Added a protect command that allows users to toggle whether stacks should be protected from destroy calls 179 | * Refactored the deployment locking logic 180 | * Corrected the destroy command so it no longer creates a deployment 181 | * Gracefully exit if the .simple_deploy.yml file is missing or corrupt 182 | * Corrected the update command so it now checks the deployment lock at the beginning of the process 183 | 184 | ## v0.4.8: 185 | 186 | * Added as_command_args option to attributes cli 187 | 188 | ## v0.4.7: 189 | 190 | * Added specs 191 | * Updated deployment to set primary host properly in classic 192 | * Updated ssh to return classic commands properly 193 | 194 | ## v0.4.6: 195 | 196 | * Added spec tests 197 | * Passing logger object into Capistrano deploy 198 | * Updated logger to accept puts from Capistrano 199 | * Refactored deployment 200 | * Added support for both classic & vpc deployments 201 | * Removed unused stack_reader & stack_lister classes 202 | 203 | ## v0.4.5: 204 | 205 | * Corrected SSL verification options 206 | 207 | ## v0.4.4: 208 | 209 | * Set verify SSL to false for real 210 | 211 | ## v0.4.3: 212 | 213 | * Gracefully exit if no notification settings 214 | * Send info level notification starting message 215 | * Set verify SSL to false 216 | 217 | ## v0.4.2: 218 | 219 | * Upgrade to stackster v0.2.9 220 | * Added -v to main cli options 221 | 222 | ## v0.4.1: 223 | 224 | * Added notifier class and campfire notifications 225 | * Added primary host env to deployment command 226 | * Upgraded to stackster v0.2.8 227 | 228 | ## v0.4.0: 229 | 230 | * Added support for passing updates with deploy command 231 | * Added support for deploying updates to multiple stacks 232 | * Updated log outputs 233 | * Abstracted CLI into individual classes 234 | 235 | ## v0.3.7: 236 | 237 | * Updated stackster 238 | 239 | ## v0.3.6: 240 | 241 | * Added -l to set log level 242 | * Change -l to -c to limit results 243 | * Updated output of ssh command 244 | 245 | ## v0.3.5: 246 | 247 | * Added ssh command 248 | 249 | ## v0.3.4: 250 | 251 | * List stacks in order based on name 252 | 253 | ## v0.3.3: 254 | 255 | * Exist if -e is not a valid environment 256 | * Updated to version 0.2.4 of stackster 257 | 258 | ## v0.3.2: 259 | 260 | * Improved debuging 261 | * Added version 262 | * Updated to version 0.2.3 of stackster 263 | 264 | ## v0.3.1: 265 | 266 | * Fixed bug in creating new stacks 267 | 268 | ## v0.3.0: 269 | 270 | * Updating to new ~/.simpledeploy.yml format. 271 | 272 | artifacts: 273 | chef_repo: 274 | bucket_prefix: prefix_to_bucket 275 | domain: name_of_object 276 | app: 277 | bucket_prefix: prefix_to_bucket 278 | domain: name_of_object 279 | cookbooks: 280 | bucket_prefix: prefix_to_bucket 281 | 282 | environments: 283 | preprod_shared_us-west-1: 284 | access_key: XXX 285 | secret_key: YYY 286 | region: us-west-1 287 | infrastructure_us-west-1: 288 | access_key: XXX 289 | secret_key: YYY 290 | region: us-west-1 291 | infrastructure_us-east-1: 292 | access_key: XXX 293 | secret_key: YYY 294 | region: us-east-1 295 | 296 | * Adding locking to deployment. 297 | * Added limit to number of events coming out with -l flag. 298 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in simple_deploy.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Intuit Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/intuit/simple_deploy.png)](http://travis-ci.org/intuit/simple_deploy) 2 | 3 | **Simple Deploy is in maintenance mode!** 4 | 5 | **We will continue to provide support for bug fix requests, however new 6 | features will not be added.** 7 | 8 | It has been a good run, but it has now come to an end. When Simple Deploy was started, 9 | the [AWS CLI](http://aws.amazon.com/cli/) had minimal Cloud Formation support; that 10 | is no longer the case. The AWS CLI is mature, well-documented and can be used 11 | to perform the majority of the Cloud Formation actions provided by Simple Deploy 12 | and we believe new customers will be better served leveraging it for their Cloud 13 | Formation stack management. 14 | 15 | For the features which Simple Deploy provides that are not available via AWS CLI, 16 | we have created the following utilities to provide near-like services, to what is 17 | provided by Simple Deploy, which can be integrated with the AWS CLI. 18 | 19 | * [cfn-clone](https://github.com/intuit/cfn-clone) allows for cloning Cloud Formation 20 | stacks. It will leverage the AWS CLI to read the parameters and template from an existing 21 | stack. It allows you to override either the template or inputs and create a new stack 22 | with the updated attirbutes and template. 23 | 24 | * [heirloom-url](https://github.com/intuit/heirloom-url) generates URLs that point to resources 25 | which have been uploaded to Heirloom. This can be coupled with the AWS CLI to update 26 | the app or chef_repo. 27 | 28 | For example, to update the app and chef_repo of a stack, and then kick off a command, 29 | similiar to the Simple Deploy deploy subcommand, you could use the following bash script. 30 | 31 | ``` 32 | #!/bin/bash 33 | 34 | app_id=$1 35 | chef_id=$2 36 | 37 | app_url=`heirloom-url -bucket-prefix=bp -domain=my-app -encrypted=true -id=$app_id -region=us-west-2` 38 | chef_url=`heirloom-url -bucket-prefix=bp -domain=my-chef -encrypted=true -id=$chef_id -region=us-west-2` 39 | 40 | aws cloudformation update-stack --stack-name my-app-stack \ 41 | --parameters AppArtifactURL=$app_url,ChefRepoURL=$chef_url 42 | 43 | ips=`aws ec2 describe-instances --filter Name=Name,Values=my-app-stack \ 44 | | jq --raw-output '.Reservations[].Instances[].PublicIpAddress'` 45 | 46 | for ip in $ips; do 47 | ssh ip 'chef-solo -c /var/chef/config/solo.rb -o role[app]’ 48 | done 49 | ``` 50 | 51 | If you are interested in more efficient SSH parallelization, I would look into one 52 | of the below tools which could be integrated into the above script. 53 | 54 | * [Capistrano](http://capistranorb.com/) 55 | * [GNU Parallel](http://www.gnu.org/software/parallel/) 56 | 57 | Overview 58 | -------- 59 | 60 | Simple Deploy is an opinionated gem that helps manage and perform directed deployments to AWS Cloud Formation Stacks. 61 | 62 | Prerequisites 63 | ------------- 64 | 65 | * Ruby version 1.9.2 or higher installed. 66 | * AWS account access key and secret key. 67 | 68 | Installation 69 | ------------ 70 | 71 | Install the gem 72 | 73 | ``` 74 | gem install simple_deploy --no-ri --no-rdoc 75 | ``` 76 | 77 | Create a file **~/.simple_deploy.yml** and include within it: 78 | 79 | ``` 80 | environments: 81 | preprod: 82 | access_key: XXX 83 | secret_key: yyy 84 | region: us-west-1 85 | ``` 86 | 87 | Documentation 88 | ------------- 89 | 90 | For more information, please view the [Simple Deploy Wiki](https://github.com/intuit/simple_deploy/wiki). 91 | 92 | Contributing 93 | ------------- 94 | 95 | 1. Fork it 96 | 2. Create your feature branch (`git checkout -b my-new-feature`) 97 | 3. Commit your changes (`git commit -am 'Add some feature'`) 98 | 4. Push to the branch (`git push origin my-new-feature`) 99 | 5. Create new Pull Request 100 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rspec/core/rake_task' 3 | 4 | desc 'Run specs' 5 | 6 | RSpec::Core::RakeTask.new do |t| 7 | t.rspec_opts = %w(--color) 8 | end 9 | -------------------------------------------------------------------------------- /bin/simple_deploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'simple_deploy' 4 | require 'simple_deploy/cli' 5 | 6 | SimpleDeploy::CLI.start 7 | -------------------------------------------------------------------------------- /lib/simple_deploy.rb: -------------------------------------------------------------------------------- 1 | require 'simple_deploy/aws' 2 | require 'simple_deploy/env' 3 | require 'simple_deploy/entry' 4 | require 'simple_deploy/entry_lister' 5 | require 'simple_deploy/exceptions' 6 | require 'simple_deploy/configuration' 7 | require 'simple_deploy/artifact' 8 | require 'simple_deploy/stack' 9 | require 'simple_deploy/misc' 10 | require 'simple_deploy/template' 11 | require 'simple_deploy/notifier' 12 | require 'simple_deploy/logger' 13 | require 'simple_deploy/version' 14 | require 'simple_deploy/backoff' 15 | 16 | module SimpleDeploy 17 | module_function 18 | 19 | def create_config(environment, custom_config = {}) 20 | raise SimpleDeploy::Exceptions::IllegalStateException.new( 21 | 'environment is not defined') unless environment 22 | 23 | @config = SimpleDeploy::Configuration.configure environment, custom_config 24 | end 25 | 26 | def config 27 | @config 28 | end 29 | 30 | def release_config 31 | @config = nil 32 | end 33 | 34 | def environments(custom_config = {}) 35 | SimpleDeploy::Configuration.environments custom_config 36 | end 37 | 38 | def logger(log_level = 'info') 39 | @logger ||= SimpleDeployLogger.new :log_level => log_level 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/simple_deploy/artifact.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class Artifact 3 | 4 | def initialize(args) 5 | @bucket_prefix = args[:bucket_prefix] 6 | @id = args[:id] 7 | @name = args[:name] 8 | @region = args[:region] 9 | @domain = args[:domain] 10 | @encrypted = args[:encrypted] 11 | 12 | @bucket = "#{@bucket_prefix}-#{@region}" 13 | @key = @encrypted ? "#{@id}.tar.gz.gpg" : "#{@id}.tar.gz" 14 | end 15 | 16 | def endpoints 17 | { 's3' => s3_url, 'http' => http_url, 'https' => https_url } 18 | end 19 | 20 | private 21 | 22 | def s3_url 23 | "s3://#{@bucket}/#{@domain}/#{@key}" 24 | end 25 | 26 | def http_url 27 | "http://#{s3_endpoints[@region]}/#{@bucket}/#{@domain}/#{@key}" 28 | end 29 | 30 | def https_url 31 | "https://#{s3_endpoints[@region]}/#{@bucket}/#{@domain}/#{@key}" 32 | end 33 | 34 | def s3_endpoints 35 | { 36 | 'us-east-1' => 's3.amazonaws.com', 37 | 'us-west-1' => 's3-us-west-1.amazonaws.com', 38 | 'us-west-2' => 's3-us-west-2.amazonaws.com' 39 | } 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/simple_deploy/aws.rb: -------------------------------------------------------------------------------- 1 | require "simple_deploy/aws/helpers" 2 | require "simple_deploy/aws/cloud_formation" 3 | require "simple_deploy/aws/cloud_formation/error" 4 | require "simple_deploy/aws/instance_reader" 5 | require "simple_deploy/aws/simpledb" 6 | -------------------------------------------------------------------------------- /lib/simple_deploy/aws/cloud_formation.rb: -------------------------------------------------------------------------------- 1 | require 'fog' 2 | 3 | module SimpleDeploy 4 | class AWS 5 | class CloudFormation 6 | 7 | include Helpers 8 | 9 | def initialize 10 | @config = SimpleDeploy.config 11 | @logger = SimpleDeploy.logger 12 | @connect = Fog::AWS::CloudFormation.new connection_args 13 | end 14 | 15 | def create(args) 16 | parameters = { 'Parameters' => args[:parameters] } 17 | data = { 'Capabilities' => ['CAPABILITY_IAM'], 18 | 'TemplateBody' => args[:template] }.merge parameters 19 | @connect.create_stack(args[:name], data) 20 | @logger.info "Cloud Formation stack creation completed." 21 | rescue Exception => e 22 | Error.new(:exception => e).process 23 | end 24 | 25 | def update(args) 26 | parameters = { 'Parameters' => args[:parameters] } 27 | data = { 'Capabilities' => ['CAPABILITY_IAM'], 28 | 'TemplateBody' => args[:template] }.merge parameters 29 | @connect.update_stack(args[:name], data) 30 | @logger.info "Cloud Formation stack update completed." 31 | rescue Exception => e 32 | Error.new(:exception => e).process 33 | end 34 | 35 | def destroy(name) 36 | @connect.delete_stack name 37 | @logger.info "Cloud Formation stack destroy completed." 38 | rescue Exception => e 39 | Error.new(:exception => e).process 40 | end 41 | 42 | def describe_stack(name) 43 | @connect.describe_stacks('StackName' => name).body['Stacks'] 44 | rescue Exception => e 45 | Error.new(:exception => e).process 46 | end 47 | 48 | def stack_resources(name) 49 | @connect.describe_stack_resources('StackName' => name).body['StackResources'] 50 | rescue Exception => e 51 | Error.new(:exception => e).process 52 | end 53 | 54 | def stack_events(name, limit) 55 | @connect.describe_stack_events(name).body['StackEvents'] [0..limit-1] 56 | rescue Exception => e 57 | Error.new(:exception => e).process 58 | end 59 | 60 | def stack_status(name) 61 | describe_stack(name).first['StackStatus'] 62 | end 63 | 64 | def stack_outputs(name) 65 | describe_stack(name).last['Outputs'] 66 | end 67 | 68 | def template(name) 69 | @connect.get_template(name).body['TemplateBody'] 70 | rescue Exception => e 71 | Error.new(:exception => e).process 72 | end 73 | 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/simple_deploy/aws/cloud_formation/error.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class AWS 3 | class CloudFormation 4 | class Error 5 | 6 | def initialize(args) 7 | @logger = SimpleDeploy.logger 8 | @exception = args[:exception] 9 | end 10 | 11 | def process 12 | message = @exception.message 13 | unless message.empty? 14 | case message 15 | when 'No updates are to be performed.' 16 | @logger.info message 17 | when /^Stack:(.*) does not exist$/ 18 | @logger.error message 19 | raise Exceptions::UnknownStack.new message 20 | else 21 | @logger.error message 22 | raise Exceptions::CloudFormationError.new message 23 | end 24 | else 25 | @logger.error "Unknown exception from cloudformation #{@exception.inspect}" 26 | raise Exceptions::CloudFormationError.new "Unknown exception from cloudformation" 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/simple_deploy/aws/helpers.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class AWS 3 | module Helpers 4 | 5 | def connection_args 6 | { 7 | aws_access_key_id: @config.access_key, 8 | aws_secret_access_key: @config.secret_key, 9 | region: @config.region 10 | }.tap do |a| 11 | 12 | if @config.temporary_credentials? 13 | a.merge!({ aws_session_token: @config.security_token }) 14 | end 15 | end 16 | end 17 | 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/simple_deploy/aws/instance_reader.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class AWS 3 | class InstanceReader 4 | 5 | include Helpers 6 | 7 | def initialize 8 | @config = SimpleDeploy.config 9 | @asg_connect = Fog::AWS::AutoScaling.new connection_args 10 | @ec2_connect = Fog::Compute::AWS.new connection_args 11 | end 12 | 13 | def list_stack_instances(stack_name) 14 | 15 | instances = [] 16 | 17 | #Nested stack 18 | nested_stacks = nested_stacks_names(stack_name) 19 | instances = nested_stacks.map {|stack| list_stack_instances stack }.flatten if nested_stacks.any? 20 | 21 | #Auto Scaling Group 22 | asg_ids = auto_scaling_group_id(stack_name) 23 | asg_instances = asg_ids.map { |asg_id| list_instances asg_id }.flatten 24 | 25 | #EC2 instance 26 | stack_instances = instance_names(stack_name) 27 | 28 | instances += (describe_instances (asg_instances + stack_instances)) if (asg_instances + stack_instances).any? 29 | 30 | instances 31 | end 32 | 33 | private 34 | 35 | def list_instances(asg_id) 36 | body = @asg_connect.describe_auto_scaling_groups('AutoScalingGroupNames' => [asg_id]).body 37 | result = body['DescribeAutoScalingGroupsResult']['AutoScalingGroups'].last 38 | return [] unless result 39 | 40 | result['Instances'].map { |info| info['InstanceId'] } 41 | end 42 | 43 | def describe_instances(instances) 44 | @ec2_connect.describe_instances('instance-state-name' => 'running', 45 | 'instance-id' => instances).body['reservationSet'] 46 | end 47 | 48 | def cloud_formation 49 | @cloud_formation ||= AWS::CloudFormation.new 50 | end 51 | 52 | def auto_scaling_group_id(stack_name) 53 | cf_stack_resources = cloud_formation.stack_resources stack_name 54 | parse_cf_stack_resources cf_stack_resources 55 | end 56 | 57 | def parse_cf_stack_resources(cf_stack_resources) 58 | asgs = cf_stack_resources.select do |r| 59 | r['ResourceType'] == 'AWS::AutoScaling::AutoScalingGroup' 60 | end 61 | asgs.any? ? asgs.map {|asg| asg['PhysicalResourceId'] } : [] 62 | end 63 | 64 | def nested_stacks_names(stack_name) 65 | cf_stack_resources = cloud_formation.stack_resources stack_name 66 | asgs = cf_stack_resources.select do |r| 67 | r['ResourceType'] == 'AWS::CloudFormation::Stack' && cloud_formation.stack_status(r['PhysicalResourceId']) != 'DELETE_COMPLETE' 68 | end 69 | asgs.any? ? asgs.map {|asg| asg['PhysicalResourceId'] } : [] 70 | end 71 | 72 | def instance_names(stack_name) 73 | cf_stack_resources = cloud_formation.stack_resources stack_name 74 | asgs = cf_stack_resources.select do |r| 75 | r['ResourceType'] == 'AWS::EC2::Instance' 76 | end 77 | asgs.any? ? asgs.map {|asg| asg['PhysicalResourceId'] } : [] 78 | end 79 | 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/simple_deploy/aws/simpledb.rb: -------------------------------------------------------------------------------- 1 | require 'fog' 2 | require 'retries' 3 | 4 | module SimpleDeploy 5 | class AWS 6 | class SimpleDB 7 | 8 | include Helpers 9 | 10 | def initialize 11 | @config = SimpleDeploy.config 12 | @connect = Fog::AWS::SimpleDB.new connection_args 13 | end 14 | 15 | def retry_options 16 | {:max_retries => 3, 17 | :rescue => Excon::Errors::ServiceUnavailable, 18 | :base_sleep_seconds => 10, 19 | :max_sleep_seconds => 60} 20 | end 21 | 22 | def domains 23 | with_retries(retry_options) do 24 | @connect.list_domains.body['Domains'] 25 | end 26 | end 27 | 28 | def domain_exists?(domain) 29 | domains.include? domain 30 | end 31 | 32 | def create_domain(domain) 33 | with_retries(retry_options) do 34 | @connect.create_domain(domain) unless domain_exists?(domain) 35 | end 36 | end 37 | 38 | def put_attributes(domain, key, attributes, options) 39 | with_retries(retry_options) do 40 | @connect.put_attributes domain, key, attributes, options 41 | end 42 | end 43 | 44 | def select(query) 45 | options = { 'ConsistentRead' => true } 46 | data = {} 47 | next_token = nil 48 | 49 | while true 50 | options.merge! 'NextToken' => next_token 51 | chunk = with_retries(retry_options) do 52 | @connect.select(query, options).body 53 | end 54 | data.merge! chunk['Items'] 55 | next_token = chunk['NextToken'] 56 | break unless next_token 57 | end 58 | 59 | data 60 | end 61 | 62 | def delete(domain, key) 63 | with_retries(retry_options) do 64 | @connect.delete_attributes domain, key 65 | end 66 | end 67 | 68 | def delete_items(domain, key, attributes) 69 | with_retries(retry_options) do 70 | @connect.delete_attributes domain, key, attributes 71 | end 72 | end 73 | 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/simple_deploy/backoff.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class Backoff 3 | def self.exp_periods(num_periods = 3) 4 | (1..num_periods).each do |n| 5 | yield (2.0**n).ceil 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | require 'simple_deploy/cli/shared' 4 | 5 | require 'simple_deploy/cli/attributes' 6 | require 'simple_deploy/cli/clone' 7 | require 'simple_deploy/cli/create' 8 | require 'simple_deploy/cli/deploy' 9 | require 'simple_deploy/cli/destroy' 10 | require 'simple_deploy/cli/environments' 11 | require 'simple_deploy/cli/events' 12 | require 'simple_deploy/cli/execute' 13 | require 'simple_deploy/cli/instances' 14 | require 'simple_deploy/cli/list' 15 | require 'simple_deploy/cli/outputs' 16 | require 'simple_deploy/cli/parameters' 17 | require 'simple_deploy/cli/protect' 18 | require 'simple_deploy/cli/resources' 19 | require 'simple_deploy/cli/status' 20 | require 'simple_deploy/cli/template' 21 | require 'simple_deploy/cli/update' 22 | 23 | module SimpleDeploy 24 | module CLI 25 | 26 | def self.start 27 | cmd = ARGV.shift 28 | 29 | case cmd 30 | when 'attributes' 31 | CLI::Attributes.new.show 32 | when 'clone' 33 | CLI::Clone.new.clone 34 | when 'create' 35 | CLI::Create.new.create 36 | when 'destroy', 'delete' 37 | CLI::Destroy.new.destroy 38 | when 'deploy' 39 | CLI::Deploy.new.deploy 40 | when 'environments', 'envs' 41 | CLI::Environments.new.environments 42 | when 'events' 43 | CLI::Events.new.show 44 | when 'execute' 45 | CLI::Execute.new.execute 46 | when 'instances' 47 | CLI::Instances.new.list 48 | when 'list' 49 | CLI::List.new.stacks 50 | when 'outputs' 51 | CLI::Outputs.new.show 52 | when 'parameters' 53 | CLI::Parameters.new.show 54 | when 'protect' 55 | CLI::Protect.new.protect 56 | when 'resources' 57 | CLI::Resources.new.show 58 | when 'status' 59 | CLI::Status.new.show 60 | when 'template' 61 | CLI::Template.new.show 62 | when 'update' 63 | CLI::Update.new.update 64 | when '-h' 65 | usage 66 | when '-v' 67 | puts SimpleDeploy::VERSION 68 | else 69 | puts "Unknown command: '#{cmd}'." 70 | puts '' 71 | usage 72 | exit 1 73 | end 74 | end 75 | 76 | def self.usage 77 | puts 'Usage: simple_deploy command' 78 | puts '' 79 | puts 'Append -h for help on specific subcommand.' 80 | puts '' 81 | 82 | puts 'Commands:' 83 | commands.each do |cmd| 84 | $stdout.printf " %-#{length_of_longest_command}s %s\n", 85 | cmd.command_name, 86 | cmd.command_summary 87 | end 88 | end 89 | 90 | def self.commands 91 | return @commands if @commands 92 | klasses = SimpleDeploy::CLI.constants.reject { |c| c == :Shared } 93 | @commands = klasses.map { |klass| SimpleDeploy::CLI.const_get(klass).new } 94 | end 95 | 96 | def self.length_of_longest_command 97 | commands.map { |c| c.command_name.length }.max 98 | end 99 | 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/attributes.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Attributes 7 | include Shared 8 | 9 | def show 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | Show attributes for stack. 15 | 16 | simple_deploy attributes -n STACK_NAME -e ENVIRONMENT 17 | 18 | EOS 19 | opt :help, "Display Help" 20 | opt :as_command_args, 21 | "Displays the attributes in a format suitable for using on the command line" 22 | opt :environment, "Set the target environment", :type => :string 23 | opt :log_level, "Log level: debug, info, warn, error", :type => :string, 24 | :default => 'info' 25 | opt :name, "Stack name to manage", :type => :string 26 | opt :read_from_env, "Read credentials and region from environment variables" 27 | end 28 | 29 | valid_options? :provided => @opts, 30 | :required => [:environment, :name, :read_from_env] 31 | 32 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 33 | SimpleDeploy.create_config config_arg 34 | SimpleDeploy.logger @opts[:log_level] 35 | @stack = Stack.new :name => @opts[:name], 36 | :environment => @opts[:environment] 37 | 38 | @opts[:as_command_args] ? command_args_output : default_output 39 | end 40 | 41 | def command_summary 42 | 'Show attributes for stack' 43 | end 44 | 45 | private 46 | 47 | def attribute_data 48 | rescue_exceptions_and_exit do 49 | Hash[@stack.attributes.sort] 50 | end 51 | end 52 | 53 | def command_args_output 54 | puts attribute_data.map { |k, v| "-a #{k}=#{v}" }.join(' ') 55 | end 56 | 57 | def default_output 58 | attribute_data.each_pair { |k, v| puts "#{k}: #{v}" } 59 | end 60 | end 61 | 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/clone.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | require 'tempfile' 3 | 4 | module SimpleDeploy 5 | module CLI 6 | 7 | class Clone 8 | include Shared 9 | 10 | def clone 11 | @opts = Trollop::options do 12 | version SimpleDeploy::VERSION 13 | banner <<-EOS 14 | 15 | Clone a stack. 16 | 17 | simple_deploy clone -s SOURCE_STACK_NAME -n NEW_STACK_NAME -e ENVIRONMENT -a ATTRIB1=VALUE -a ATTRIB2=VALUE 18 | 19 | EOS 20 | opt :help, "Display Help" 21 | opt :environment, "Set the target environment", :type => :string 22 | opt :input_stack, "Read outputs from given stack(s) and map them \ 23 | to parameter inputs in the new stack. These will be passed to inputs with \ 24 | matching or pluralized names. Can be specified multiple times.", :type => :string, 25 | :multi => true 26 | opt :log_level, "Log level: debug, info, warn, error", :type => :string, 27 | :default => 'info' 28 | opt :source_name, "Stack name for the stack to clone", :type => :string 29 | opt :new_name, "Stack name for the new stack", :type => :string 30 | opt :attributes, "= separated attribute and it's value", :type => :string, 31 | :multi => true 32 | opt :template, "Path to a new template file", :type => :string 33 | opt :read_from_env, "Read credentials and region from environment variables" 34 | end 35 | 36 | valid_options? :provided => @opts, 37 | :required => [:environment, 38 | :source_name, 39 | :new_name, 40 | :read_from_env] 41 | 42 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 43 | SimpleDeploy.create_config config_arg 44 | SimpleDeploy.logger @opts[:log_level] 45 | 46 | override_attributes = parse_attributes :attributes => @opts[:attributes] 47 | cloned_attributes = filter_attributes source_stack.attributes 48 | 49 | template_file = Tempfile.new("#{@opts[:new_name]}_template.json") 50 | template_file_path = template_file.path 51 | 52 | if @opts[:template] 53 | template_file_path = @opts[:template] 54 | else 55 | template_file.write source_stack.template 56 | end 57 | 58 | if @opts[:input_stack] 59 | input_attributes = mapper.map_outputs_from_stacks :stacks => @opts[:input_stack], 60 | :template => template_file_path 61 | new_overrides = merge_attributes input_attributes, override_attributes 62 | new_overrides += add_attributes input_attributes, override_attributes 63 | else 64 | new_overrides = override_attributes 65 | end 66 | 67 | new_attributes = merge_attributes cloned_attributes, new_overrides 68 | new_attributes += add_attributes cloned_attributes, new_overrides 69 | 70 | rescue_exceptions_and_exit do 71 | new_stack.create :attributes => new_attributes, 72 | :template => template_file_path 73 | end 74 | 75 | template_file.close 76 | end 77 | 78 | def command_summary 79 | 'Clone a stack' 80 | end 81 | 82 | private 83 | def filter_attributes(source_attributes) 84 | selected = source_attributes.select { |k| k !~ /^deployment/ } 85 | selected.map { |k,v| { k => v } } 86 | end 87 | 88 | def merge_attributes(cloned_attributes, override_attributes) 89 | cloned_attributes.map do |clone| 90 | key = clone.keys.first 91 | override = override_attributes.find { |o| o.has_key? key } 92 | override ? override : clone 93 | end 94 | end 95 | 96 | def add_attributes(cloned_attributes, override_attributes) 97 | override_attributes.map do |override| 98 | key = override.keys.first 99 | clone = cloned_attributes.find { |c| c.has_key? key } 100 | clone ? nil : override 101 | end.compact 102 | end 103 | 104 | def mapper 105 | @om ||= Stack::OutputMapper.new :environment => @opts[:environment] 106 | end 107 | 108 | def source_stack 109 | @source_stack = Stack.new :name => @opts[:source_name], 110 | :environment => @opts[:environment] 111 | end 112 | 113 | def new_stack 114 | @new_stack = Stack.new :name => @opts[:new_name], 115 | :environment => @opts[:environment] 116 | end 117 | end 118 | 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/create.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Create 7 | include Shared 8 | 9 | def create 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | Create a new stack. 15 | 16 | simple_deploy create -n STACK_NAME -t PATH_TO_TEMPLATE -e ENVIRONMENT -a KEY1=VAL1 -a KEY2=VAL2 17 | 18 | EOS 19 | opt :help, "Display Help" 20 | opt :attributes, "= seperated attribute(s) and it's value. \ 21 | Can be specified multiple times.", :type => :string, 22 | :multi => true 23 | opt :input_stack, "Read outputs from given stack(s) and map them \ 24 | to parameter inputs in the new stack. These will be passed to inputs with \ 25 | matching or pluralized names. Can be specified multiple times.", :type => :string, 26 | :multi => true 27 | opt :environment, "Set the target environment", :type => :string 28 | opt :log_level, "Log level: debug, info, warn, error", :type => :string, 29 | :default => 'info' 30 | opt :name, "Stack name(s) of stack to deploy", :type => :string 31 | opt :read_from_env, "Read credentials and region from environment variables" 32 | opt :template, "Path to the template file", :type => :string 33 | end 34 | 35 | valid_options? :provided => @opts, 36 | :required => [:environment, :name, :read_from_env, :template] 37 | 38 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 39 | SimpleDeploy.create_config config_arg 40 | SimpleDeploy.logger @opts[:log_level] 41 | stack = Stack.new :name => @opts[:name], 42 | :environment => @opts[:environment] 43 | 44 | rescue_exceptions_and_exit do 45 | stack.create :attributes => merged_attributes, 46 | :template => @opts[:template] 47 | end 48 | end 49 | 50 | def command_summary 51 | 'Create a new stack' 52 | end 53 | 54 | private 55 | 56 | def merged_attributes 57 | provided_attributes = parse_attributes :attributes => @opts[:attributes] 58 | 59 | attribute_merger.merge :attributes => provided_attributes, 60 | :environment => @opts[:environment], 61 | :input_stacks => @opts[:input_stack], 62 | :template => @opts[:template] 63 | end 64 | 65 | def attribute_merger 66 | SimpleDeploy::Misc::AttributeMerger.new 67 | end 68 | end 69 | 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/deploy.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Deploy 7 | include Shared 8 | 9 | def deploy 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | Execute deployment on given stack(s). 15 | 16 | simple_deploy deploy -n STACK_NAME -n STACK_NAME -e ENVIRONMENT 17 | 18 | Using SSH: 19 | 20 | Simple deploy defaults your user and key for SSH to your username and your id_rsa key. 21 | 22 | If you need to override these because you want to use a different username or you have a different key file, 23 | you can set simple deploy specific environment variables to do the override. 24 | 25 | Example 1: Overriding when the command is run. 26 | SIMPLE_DEPLOY_SSH_USER=fred SIMPLE_DEPLOY_SSH_KEY=$HOME/.ssh/id_dsa simple_deploy deploy -n STACK_NAME -n STACK_NAME -e ENVIRONMENT 27 | 28 | Example 2: Overriding them in your shell environment (bash shell used in the example). 29 | export SIMPLE_DEPLOY_SSH_USER=fred 30 | export SIMPLE_DEPLOY_SSH_KEY=$HOME/.ssh/id_dsa 31 | simple_deploy deploy -n STACK_NAME -n STACK_NAME -e ENVIRONMENT 32 | 33 | Using Internal / External IP for SSH: 34 | 35 | simple_deploy defaults to using the public IP when ssh'ng to stacks in classic, or the private IP when in a VPC. 36 | 37 | The internal or external flag forces simple_deploy to use the given IP address. 38 | 39 | simple_deploy deploy -n STACK_NAME -n STACK_NAME -e ENVIRONMENT -i 40 | 41 | EOS 42 | opt :help, "Display Help" 43 | opt :attributes, "= seperated attribute and it's value", :type => :string, 44 | :multi => true 45 | opt :environment, "Set the target environment", :type => :string 46 | opt :force, "Force a deployment to proceed" 47 | opt :log_level, "Log level: debug, info, warn, error", :type => :string, 48 | :default => 'info' 49 | opt :name, "Stack name(s) of stack to deploy", :type => :string, 50 | :multi => true 51 | opt :quiet, "Quiet, do not send notifications" 52 | opt :read_from_env, "Read credentials and region from environment variables" 53 | opt :external, "Use external IP for ssh commands" 54 | opt :internal, "Use internal IP for ssh commands" 55 | end 56 | 57 | valid_options? :provided => @opts, 58 | :required => [:environment, :name, :read_from_env] 59 | 60 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 61 | SimpleDeploy.create_config config_arg 62 | logger = SimpleDeploy.logger @opts[:log_level] 63 | 64 | new_attributes = parse_attributes :attributes => @opts[:attributes] 65 | 66 | @opts[:name].each do |name| 67 | notifier = Notifier.new :stack_name => name, 68 | :environment => @opts[:environment] 69 | 70 | stack = Stack.new :name => name, 71 | :environment => @opts[:environment], 72 | :external => @opts[:external], 73 | :internal => @opts[:internal] 74 | 75 | proceed = true 76 | 77 | if new_attributes.any? 78 | rescue_exceptions_and_exit do 79 | proceed = stack.update :force => @opts[:force], 80 | :attributes => new_attributes 81 | end 82 | end 83 | 84 | stack.wait_for_stable 85 | 86 | if proceed 87 | notifier.send_deployment_start_message unless @opts[:quiet] 88 | 89 | result = stack.deploy @opts[:force] 90 | 91 | if result 92 | notifier.send_deployment_complete_message unless @opts[:quiet] 93 | else 94 | logger.error "Deployment to #{name} did not complete successfully." 95 | exit 1 96 | end 97 | else 98 | logger.error "Update of #{name} did not complete successfully." 99 | exit 1 100 | end 101 | 102 | end 103 | end 104 | 105 | def command_summary 106 | 'Execute deployment on given stack(s)' 107 | end 108 | 109 | end 110 | 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/destroy.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Destroy 7 | include Shared 8 | 9 | def destroy 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | Destroy a stack. 15 | 16 | simple_deploy destroy -n STACK_NAME -e ENVIRONMENT 17 | 18 | EOS 19 | opt :help, "Display Help" 20 | opt :environment, "Set the target environment", :type => :string 21 | opt :log_level, "Log level: debug, info, warn, error", :type => :string, 22 | :default => 'info' 23 | opt :name, "Stack name(s) of stack to deploy", :type => :string 24 | opt :read_from_env, "Read credentials and region from environment variables" 25 | end 26 | 27 | valid_options? :provided => @opts, 28 | :required => [:environment, :name, :read_from_env] 29 | 30 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 31 | SimpleDeploy.create_config config_arg 32 | SimpleDeploy.logger @opts[:log_level] 33 | stack = Stack.new :name => @opts[:name], 34 | :environment => @opts[:environment] 35 | 36 | exit 1 unless stack.destroy 37 | end 38 | 39 | def command_summary 40 | 'Destroy a stack' 41 | end 42 | 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/environments.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Environments 7 | include Shared 8 | 9 | def environments 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | List environments 15 | 16 | simple_deploy environments 17 | 18 | EOS 19 | opt :help, "Display Help" 20 | end 21 | 22 | SimpleDeploy.environments.keys.each { |e| puts e } 23 | end 24 | 25 | def command_summary 26 | 'List environments' 27 | end 28 | 29 | end 30 | 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/events.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Events 7 | include Shared 8 | 9 | def show 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | Show events for stack. 15 | 16 | simple_deploy attributes -n STACK_NAME -e ENVIRONMENT 17 | 18 | EOS 19 | opt :help, "Display Help" 20 | opt :count, "Count of events returned.", :type => :integer, 21 | :default => 3 22 | opt :environment, "Set the target environment", :type => :string 23 | opt :name, "Stack name to manage", :type => :string 24 | opt :read_from_env, "Read credentials and region from environment variables" 25 | end 26 | 27 | valid_options? :provided => @opts, 28 | :required => [:environment, :name, :read_from_env] 29 | 30 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 31 | SimpleDeploy.create_config config_arg 32 | SimpleDeploy.logger @opts[:log_level] 33 | stack = Stack.new :name => @opts[:name], 34 | :environment => @opts[:environment] 35 | 36 | rescue_exceptions_and_exit do 37 | jj stack.events @opts[:count] 38 | end 39 | end 40 | 41 | def command_summary 42 | "Show events for a stack" 43 | end 44 | 45 | end 46 | 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/execute.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Execute 7 | include Shared 8 | 9 | def execute 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | Execute command on given stack(s). 15 | 16 | simple_deploy execute -n STACK_NAME -n STACK_NAME -e ENVIRONMENT -c "COMMAND" 17 | 18 | Using Internal / External IP for SSH: 19 | 20 | simple_deploy defaults to using the public IP when ssh'ng to stacks in classic, or the private IP when in a VPC. 21 | 22 | The internal or external flag forces simple_deploy to use the given IP address. 23 | 24 | simple_deploy execute -n STACK_NAME -n STACK_NAME -e ENVIRONMENT -i 25 | 26 | EOS 27 | opt :help, "Display Help" 28 | opt :attributes, "= seperated attribute and it's value", :type => :string, 29 | :multi => true 30 | opt :command, "Command to execute.", :type => :string 31 | opt :environment, "Set the target environment", :type => :string 32 | opt :external, "Use external IP for ssh commands" 33 | opt :internal, "Use internal IP for ssh commands" 34 | opt :log_level, "Log level: debug, info, warn, error", :type => :string, 35 | :default => 'info' 36 | opt :name, "Stack name(s) of stack to deploy", :type => :string, 37 | :multi => true 38 | opt :pty, "Set pty to true when executing commands." 39 | opt :read_from_env, "Read credentials and region from environment variables" 40 | opt :sudo, "Execute command with sudo" 41 | end 42 | 43 | valid_options? :provided => @opts, 44 | :required => [:environment, :name, :read_from_env] 45 | 46 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 47 | SimpleDeploy.create_config config_arg 48 | logger = SimpleDeploy.logger @opts[:log_level] 49 | 50 | @opts[:name].each do |name| 51 | notifier = Notifier.new :stack_name => name, 52 | :environment => @opts[:environment] 53 | 54 | stack = Stack.new :name => name, 55 | :environment => @opts[:environment], 56 | :external => @opts[:external], 57 | :internal => @opts[:internal] 58 | 59 | begin 60 | unless stack.execute :command => @opts[:command], 61 | :sudo => @opts[:sudo], 62 | :pty => @opts[:pty] 63 | exit 1 64 | end 65 | rescue SimpleDeploy::Exceptions::NoInstances 66 | logger.error "Stack has no running instances." 67 | exit 1 68 | end 69 | end 70 | end 71 | 72 | def command_summary 73 | 'Execute command on given stack(s)' 74 | end 75 | 76 | end 77 | 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/instances.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Instances 7 | include Shared 8 | 9 | def list 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | List instances for stack. 15 | 16 | simple_deploy instances -n STACK_NAME -e ENVIRONMENT 17 | 18 | Using Internal / External IPs 19 | 20 | simple_deploy defaults to using the public IP when return the IP for stacks in classic, or the private IP when in a VPC. 21 | 22 | The internal or external flag forces simple_deploy to use the given IP address. 23 | 24 | simple_deploy instances -n STACK_NAME -n STACK_NAME -e ENVIRONMENT -i 25 | 26 | EOS 27 | opt :help, "Display Help" 28 | opt :environment, "Set the target environment", :type => :string 29 | opt :name, "Stack name to manage", :type => :string 30 | opt :external, "Return external IP for instances." 31 | opt :internal, "Return internal IP for instances." 32 | opt :read_from_env, "Read credentials and region from environment variables" 33 | end 34 | 35 | valid_options? :provided => @opts, 36 | :required => [:environment, :name, :read_from_env] 37 | 38 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 39 | SimpleDeploy.create_config config_arg 40 | logger = SimpleDeploy.logger @opts[:log_level] 41 | 42 | stack = Stack.new :name => @opts[:name], 43 | :environment => @opts[:environment], 44 | :external => @opts[:external], 45 | :internal => @opts[:internal] 46 | 47 | exit 1 unless stack.exists? 48 | 49 | instances = stack.instances 50 | 51 | if instances.nil? || instances.empty? 52 | logger.info "Stack '#{@opts[:name]}' does not have any instances." 53 | else 54 | puts instances 55 | end 56 | end 57 | 58 | def command_summary 59 | 'List instances for stack' 60 | end 61 | 62 | end 63 | 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/list.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | require 'simple_deploy/stack/stack_lister' 4 | 5 | module SimpleDeploy 6 | module CLI 7 | 8 | class List 9 | include Shared 10 | 11 | def stacks 12 | @opts = Trollop::options do 13 | version SimpleDeploy::VERSION 14 | banner <<-EOS 15 | 16 | List stacks in an environment 17 | 18 | simple_deploy list -e ENVIRONMENT 19 | 20 | EOS 21 | opt :environment, "Set the target environment", :type => :string 22 | opt :log_level, "Log level: debug, info, warn, error", :type => :string, 23 | :default => 'info' 24 | opt :read_from_env, "Read credentials and region from environment variables" 25 | opt :help, "Display Help" 26 | end 27 | 28 | valid_options? :provided => @opts, 29 | :required => [:environment, :read_from_env] 30 | 31 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 32 | SimpleDeploy.create_config config_arg 33 | 34 | SimpleDeploy.logger @opts[:log_level] 35 | 36 | stacks = SimpleDeploy::StackLister.new.all.sort 37 | puts stacks 38 | end 39 | 40 | def command_summary 41 | 'List stacks in an environment' 42 | end 43 | 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/outputs.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Outputs 7 | include Shared 8 | 9 | def show 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | Show outputs of a stack. 15 | 16 | simple_deploy outputs -n STACK_NAME -e ENVIRONMENT 17 | 18 | EOS 19 | opt :help, "Display Help" 20 | opt :as_command_args, 21 | "Displays the attributes in a format suitable for using on the command line" 22 | opt :environment, "Set the target environment", :type => :string 23 | opt :log_level, "Log level: debug, info, warn, error", :type => :string, 24 | :default => 'warn' 25 | opt :name, "Stack name to manage", :type => :string 26 | opt :read_from_env, "Read credentials and region from environment variables" 27 | end 28 | 29 | valid_options? :provided => @opts, 30 | :required => [:environment, :name, :read_from_env] 31 | 32 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 33 | SimpleDeploy.create_config config_arg 34 | logger = SimpleDeploy.logger @opts[:log_level] 35 | stack = Stack.new :name => @opts[:name], 36 | :environment => @opts[:environment] 37 | 38 | rescue_exceptions_and_exit do 39 | @outputs = stack.outputs 40 | 41 | logger.info "No outputs." unless @outputs.any? 42 | 43 | @opts[:as_command_args] ? command_args_output : default_output 44 | end 45 | end 46 | 47 | def command_summary 48 | 'Show outputs of a stack' 49 | end 50 | 51 | private 52 | 53 | def command_args_output 54 | @outputs.each do |hash| 55 | print "-a %s=%s " % [hash['OutputKey'], hash['OutputValue']] 56 | end 57 | puts "" 58 | end 59 | 60 | def default_output 61 | @outputs.each do |hash| 62 | puts "%s: %s" % [hash['OutputKey'], hash['OutputValue']] 63 | end 64 | end 65 | 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/parameters.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Parameters 7 | include Shared 8 | 9 | def show 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | Show parameters of a stack. 15 | 16 | simple_deploy parameters -n STACK_NAME -e ENVIRONMENT 17 | 18 | EOS 19 | opt :help, "Display Help" 20 | opt :environment, "Set the target environment", :type => :string 21 | opt :log_level, "Log level: debug, info, warn, error", :type => :string, 22 | :default => 'warn' 23 | opt :name, "Stack name to manage", :type => :string 24 | opt :read_from_env, "Read credentials and region from environment variables" 25 | end 26 | 27 | valid_options? :provided => @opts, 28 | :required => [:environment, :name, :read_from_env] 29 | 30 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 31 | SimpleDeploy.create_config config_arg 32 | SimpleDeploy.logger @opts[:log_level] 33 | stack = Stack.new :name => @opts[:name], 34 | :environment => @opts[:environment] 35 | 36 | rescue_exceptions_and_exit do 37 | puts stack.parameters.sort 38 | end 39 | end 40 | 41 | def command_summary 42 | 'Show parameters of a stack' 43 | end 44 | 45 | end 46 | 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/protect.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'trollop' 3 | 4 | module SimpleDeploy 5 | module CLI 6 | 7 | class Protect 8 | include Shared 9 | 10 | def protect 11 | @opts = Trollop::options do 12 | version SimpleDeploy::VERSION 13 | banner <<-EOS 14 | 15 | Protect/Unprotect one or more stacks. 16 | 17 | simple_deploy protect -n STACK_NAME1 -n STACK_NAME2 -e ENVIRONMENT -p on_off 18 | 19 | EOS 20 | opt :help, "Display Help" 21 | opt :environment, "Set the target environment", :type => :string 22 | opt :protection, "Enable/Disable protection using on/off", :type => :string 23 | opt :log_level, "Log level: debug, info, warn, error", :type => :string, 24 | :default => 'info' 25 | opt :name, "Stack name(s) of stacks to protect", :type => :string, 26 | :multi => true 27 | opt :read_from_env, "Read credentials and region from environment variables" 28 | end 29 | 30 | valid_options? :provided => @opts, 31 | :required => [:environment, :name, :read_from_env] 32 | 33 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 34 | SimpleDeploy.create_config config_arg 35 | SimpleDeploy.logger @opts[:log_level] 36 | 37 | @opts[:name].each do |name| 38 | stack = Stack.new :name => name, 39 | :environment => @opts[:environment] 40 | rescue_exceptions_and_exit do 41 | stack.update :attributes => [{ 'protection' => @opts[:protection] }] 42 | end 43 | end 44 | end 45 | 46 | def command_summary 47 | 'Protect/Unprotect one or more stacks' 48 | end 49 | 50 | end 51 | 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/resources.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Resources 7 | 8 | include Shared 9 | 10 | def show 11 | @opts = Trollop::options do 12 | version SimpleDeploy::VERSION 13 | banner <<-EOS 14 | 15 | Show resources of a stack. 16 | 17 | simple_deploy resources -n STACK_NAME -e ENVIRONMENT 18 | 19 | EOS 20 | opt :help, "Display Help" 21 | opt :environment, "Set the target environment", :type => :string 22 | opt :log_level, "Log level: debug, info, warn, error", :type => :string, 23 | :default => 'info' 24 | opt :name, "Stack name to manage", :type => :string 25 | opt :read_from_env, "Read credentials and region from environment variables" 26 | end 27 | 28 | valid_options? :provided => @opts, 29 | :required => [:environment, :name, :read_from_env] 30 | 31 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 32 | SimpleDeploy.create_config config_arg 33 | SimpleDeploy.logger @opts[:log_level] 34 | stack = Stack.new :name => @opts[:name], 35 | :environment => @opts[:environment] 36 | 37 | rescue_exceptions_and_exit do 38 | jj stack.resources 39 | end 40 | end 41 | 42 | def command_summary 43 | 'Show resources of a stack' 44 | end 45 | 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/shared.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | module CLI 3 | 4 | module Shared 5 | 6 | def parse_attributes(args) 7 | attributes = args[:attributes] 8 | attrs = [] 9 | 10 | attributes.each do |attribs| 11 | key = attribs.split('=').first.gsub(/\s+/, "") 12 | value = attribs.gsub(/^.+?=/, '') 13 | SimpleDeploy.logger.info "Read #{key}=#{value}" 14 | attrs << { key => value } 15 | end 16 | attrs 17 | end 18 | 19 | def valid_options?(args) 20 | provided = args[:provided] 21 | required = args[:required] 22 | 23 | if provided[:environment] && provided[:read_from_env] 24 | SimpleDeploy.logger.error "You cannot specify both --environment and --read-from-env" 25 | exit 1 26 | end 27 | 28 | if required.include?(:environment) && required.include?(:read_from_env) 29 | if !provided.include?(:environment) && !provided.include?(:read_from_env) 30 | msg = "Either '--environment' or '--read-from-env' is required but not specified" 31 | SimpleDeploy.logger.error msg 32 | exit 1 33 | end 34 | end 35 | 36 | required.reject { |i| [:environment, :read_from_env].include? i }.each do |opt| 37 | unless provided[opt] 38 | SimpleDeploy.logger.error "Option '#{opt} (-#{opt[0]})' required but not specified." 39 | exit 1 40 | end 41 | end 42 | 43 | validate_credential_env_vars if provided[:read_from_env] 44 | 45 | if provided[:environment] 46 | unless SimpleDeploy.environments.keys.include? provided[:environment] 47 | SimpleDeploy.logger.error "Environment '#{provided[:environment]}' does not exist." 48 | exit 1 49 | end 50 | end 51 | end 52 | 53 | def command_name 54 | self.class.name.split('::').last.downcase 55 | end 56 | 57 | def rescue_exceptions_and_exit 58 | yield 59 | rescue SimpleDeploy::Exceptions::Base 60 | exit 1 61 | end 62 | 63 | private 64 | 65 | def credential_env_vars_exist? 66 | !!ENV['AWS_ACCESS_KEY_ID'] && 67 | !!ENV['AWS_SECRET_ACCESS_KEY'] && 68 | !!ENV['AWS_REGION'] 69 | end 70 | 71 | def validate_credential_env_vars 72 | unless credential_env_vars_exist? 73 | msg = "The following environment variables must be set when using --read-from-env: " 74 | msg << "AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION" 75 | SimpleDeploy.logger.error msg 76 | exit 1 77 | end 78 | end 79 | 80 | end 81 | 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/status.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Status 7 | include Shared 8 | 9 | def show 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | Show status of a stack. 15 | 16 | simple_deploy status -n STACK_NAME -e ENVIRONMENT 17 | 18 | EOS 19 | opt :help, "Display Help" 20 | opt :environment, "Set the target environment", :type => :string 21 | opt :name, "Stack name to manage", :type => :string 22 | opt :read_from_env, "Read credentials and region from environment variables" 23 | end 24 | 25 | valid_options? :provided => @opts, 26 | :required => [:environment, :name, :read_from_env] 27 | 28 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 29 | SimpleDeploy.create_config config_arg 30 | SimpleDeploy.logger @opts[:log_level] 31 | stack = Stack.new :name => @opts[:name], 32 | :environment => @opts[:environment] 33 | 34 | rescue_exceptions_and_exit do 35 | puts stack.status 36 | end 37 | end 38 | 39 | def command_summary 40 | 'Show status of a stack' 41 | end 42 | 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/template.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Template 7 | include Shared 8 | 9 | def show 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | Show current template for stack. 15 | 16 | simple_deploy template -n STACK_NAME -e ENVIRONMENT 17 | 18 | EOS 19 | opt :help, "Display Help" 20 | opt :environment, "Set the target environment", :type => :string 21 | opt :name, "Stack name to manage", :type => :string 22 | opt :read_from_env, "Read credentials and region from environment variables" 23 | end 24 | 25 | valid_options? :provided => @opts, 26 | :required => [:environment, :name, :read_from_env] 27 | 28 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 29 | SimpleDeploy.create_config config_arg 30 | SimpleDeploy.logger @opts[:log_level] 31 | stack = Stack.new :name => @opts[:name], 32 | :environment => @opts[:environment] 33 | 34 | rescue_exceptions_and_exit do 35 | raw_json = JSON.parse stack.template 36 | puts JSON.pretty_generate raw_json 37 | end 38 | end 39 | 40 | def command_summary 41 | 'Show current template for stack' 42 | end 43 | 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/simple_deploy/cli/update.rb: -------------------------------------------------------------------------------- 1 | require 'trollop' 2 | 3 | module SimpleDeploy 4 | module CLI 5 | 6 | class Update 7 | include Shared 8 | 9 | def update 10 | @opts = Trollop::options do 11 | version SimpleDeploy::VERSION 12 | banner <<-EOS 13 | 14 | Update the attributes for one more stacks. 15 | 16 | simple_deploy update -n STACK_NAME1 -n STACK_NAME2 -e ENVIRONMENT -a KEY1=VAL1 -a KEY2=VAL2 17 | 18 | EOS 19 | opt :help, "Display Help" 20 | opt :attributes, "= seperated attribute and it's value", :type => :string, 21 | :multi => true 22 | opt :environment, "Set the target environment", :type => :string 23 | opt :force, "Force an update to proceed" 24 | opt :log_level, "Log level: debug, info, warn, error", :type => :string, 25 | :default => 'info' 26 | opt :name, "Stack name(s) of stack to deploy", :type => :string, 27 | :multi => true 28 | opt :read_from_env, "Read credentials and region from environment variables" 29 | opt :template, "Path to a new template file", :type => :string 30 | end 31 | 32 | valid_options? :provided => @opts, 33 | :required => [:environment, :name, :read_from_env] 34 | 35 | config_arg = @opts[:read_from_env] ? :read_from_env : @opts[:environment] 36 | SimpleDeploy.create_config config_arg 37 | SimpleDeploy.logger @opts[:log_level] 38 | 39 | attributes = parse_attributes :attributes => @opts[:attributes] 40 | 41 | @opts[:name].each do |name| 42 | stack = Stack.new :name => name, 43 | :environment => @opts[:environment] 44 | 45 | if @opts[:template] 46 | template_body = IO.read @opts[:template] 47 | end 48 | 49 | rescue_exceptions_and_exit do 50 | stack.update :force => @opts[:force], 51 | :template_body => template_body, 52 | :attributes => attributes 53 | end 54 | end 55 | end 56 | 57 | def command_summary 58 | 'Update the attributes for one more stacks' 59 | end 60 | 61 | end 62 | 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/simple_deploy/configuration.rb: -------------------------------------------------------------------------------- 1 | 2 | module SimpleDeploy 3 | module Configuration 4 | extend self 5 | 6 | def configure(environment, custom_config = {}) 7 | if custom_config.has_key?(:config) 8 | env_config = custom_config[:config]['environments'][environment] 9 | notifications = custom_config[:config]['notifications'] 10 | else 11 | env_config, notifications = load_appropriate_config(environment) 12 | end 13 | Config.new env_config, notifications 14 | end 15 | 16 | def environments(custom_config = {}) 17 | raw_config = custom_config.fetch(:config) { load_config_file } 18 | raw_config['environments'] 19 | end 20 | 21 | private 22 | 23 | def load_appropriate_config(env) 24 | if env == :read_from_env 25 | load_config_from_env_vars 26 | else 27 | load_config_from_file env 28 | end 29 | end 30 | 31 | def load_config_file 32 | begin 33 | YAML::load File.open(config_file) 34 | rescue Errno::ENOENT 35 | raise "#{config_file} not found" 36 | rescue ArgumentError, Psych::SyntaxError => e 37 | raise "#{config_file} is corrupt" 38 | end 39 | end 40 | 41 | def load_config_from_file(env) 42 | config = load_config_file 43 | return config['environments'][env], config['notifications'] 44 | end 45 | 46 | def load_config_from_env_vars 47 | env_config = { 48 | 'access_key' => ENV['AWS_ACCESS_KEY_ID'], 49 | 'region' => ENV['AWS_REGION'], 50 | 'secret_key' => ENV['AWS_SECRET_ACCESS_KEY'], 51 | 'security_token' => ENV['AWS_SECURITY_TOKEN'] 52 | } 53 | 54 | return env_config, {} 55 | end 56 | 57 | def config_file 58 | env_config_file || default_config_file 59 | end 60 | 61 | def env_config_file 62 | env.load 'SIMPLE_DEPLOY_CONFIG_FILE' 63 | end 64 | 65 | def default_config_file 66 | "#{env.load 'HOME'}/.simple_deploy.yml" 67 | end 68 | 69 | def env 70 | @env ||= SimpleDeploy::Env.new 71 | end 72 | 73 | class Config 74 | attr_reader :environment, :notifications 75 | 76 | def initialize(environment, notifications) 77 | raise ArgumentError.new("environment must be defined") unless environment 78 | 79 | @environment = environment 80 | @notifications = notifications 81 | end 82 | 83 | def artifacts 84 | ['chef_repo', 'cookbooks', 'app'] 85 | end 86 | 87 | def artifact_deploy_variable(artifact) 88 | name_to_variable_map = { 'chef_repo' => 'CHEF_REPO_URL', 89 | 'app' => 'APP_URL', 90 | 'cookbooks' => 'COOKBOOKS_URL' } 91 | name_to_variable_map[artifact] 92 | end 93 | 94 | def artifact_cloud_formation_url(artifact) 95 | name_to_url_map = { 'chef_repo' => 'ChefRepoURL', 96 | 'app' => 'AppArtifactURL', 97 | 'cookbooks' => 'CookbooksURL' } 98 | name_to_url_map[artifact] 99 | end 100 | 101 | def deploy_script 102 | '/opt/intu/admin/bin/configure.sh' 103 | end 104 | 105 | def access_key 106 | @environment['access_key'] 107 | end 108 | 109 | def secret_key 110 | @environment['secret_key'] 111 | end 112 | 113 | def security_token 114 | @environment['security_token'] 115 | end 116 | 117 | def region 118 | @environment['region'] 119 | end 120 | 121 | def temporary_credentials? 122 | !!security_token 123 | end 124 | 125 | private 126 | 127 | def env_home 128 | env.load 'HOME' 129 | end 130 | 131 | def env_user 132 | env.load 'USER' 133 | end 134 | 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/simple_deploy/entry.rb: -------------------------------------------------------------------------------- 1 | 2 | module SimpleDeploy 3 | class Entry 4 | attr_accessor :name 5 | 6 | def initialize(args) 7 | @domain = 'stacks' 8 | @config = SimpleDeploy.config 9 | @logger = SimpleDeploy.logger 10 | @custom_attributes = {} 11 | @name = region_specific_name args[:name] 12 | create_domain 13 | end 14 | 15 | def self.find(args) 16 | Entry.new :name => args[:name] 17 | end 18 | 19 | def attributes 20 | u = {} 21 | 22 | attrs = sdb_connect.select "select * from stacks where itemName() = '#{name}'" 23 | if attrs.has_key? name 24 | u.merge! Hash[attrs[name].map { |k,v| [k, v.first] }] 25 | end 26 | 27 | u.merge @custom_attributes 28 | end 29 | 30 | def set_attributes(a) 31 | a.each do |attribute| 32 | @custom_attributes.merge! attribute 33 | end 34 | end 35 | 36 | def save 37 | @custom_attributes.merge! 'Name' => name, 38 | 'CreatedAt' => Time.now.utc.to_s 39 | 40 | current_attributes = attributes.reject do |key,value| 41 | if value == 'nil' 42 | @logger.info "Removing attribute set to nil '#{key}'." 43 | sdb_connect.delete_items 'stacks', name, key => nil 44 | true 45 | end 46 | end 47 | 48 | current_attributes.each_pair {|k,v| @logger.debug "Setting attribute #{k}=#{v}"} 49 | 50 | sdb_connect.put_attributes 'stacks', 51 | name, 52 | current_attributes, 53 | :replace => current_attributes.keys 54 | 55 | @logger.debug "Save to SimpleDB successful." 56 | end 57 | 58 | def delete_attributes 59 | sdb_connect.delete('stacks', name) 60 | @logger.info "Delete from SimpleDB successful." 61 | end 62 | 63 | private 64 | 65 | def region_specific_name(name) 66 | "#{name}-#{@config.region}" 67 | end 68 | 69 | def create_domain 70 | sdb_connect.create_domain @domain 71 | end 72 | 73 | def sdb_connect 74 | @sdb_connect ||= AWS::SimpleDB.new 75 | end 76 | end 77 | 78 | end 79 | -------------------------------------------------------------------------------- /lib/simple_deploy/entry_lister.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class EntryLister 3 | 4 | def initialize 5 | @domain = 'stacks' 6 | @config = SimpleDeploy.config 7 | end 8 | 9 | def all 10 | if sdb_connect.domain_exists? @domain 11 | e = sdb_connect.select "select * from #{@domain}" 12 | entries = e.keys.map do |name| 13 | remove_region_from_entry(name) 14 | end 15 | end 16 | entries ? entries : [] 17 | end 18 | 19 | private 20 | 21 | def sdb_connect 22 | @sdb_connect ||= AWS::SimpleDB.new 23 | end 24 | 25 | def remove_region_from_entry(name) 26 | name.gsub(/-[a-z]{2}-[a-z]*-[0-9]{1,2}$/, '') 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/simple_deploy/env.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class Env 3 | 4 | def load(var) 5 | ENV.fetch var, nil 6 | end 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/simple_deploy/exceptions.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | module Exceptions 3 | 4 | class Base < RuntimeError 5 | attr_accessor :message 6 | 7 | def initialize(message="") 8 | @message = message 9 | end 10 | end 11 | 12 | class NoInstances < Base 13 | end 14 | 15 | class Exceptions::NoInstances < Base 16 | end 17 | 18 | class CloudFormationError < Base 19 | end 20 | 21 | class UnknownStack < Base 22 | end 23 | 24 | class IllegalStateException < Base 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/simple_deploy/logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module SimpleDeploy 4 | class SimpleDeployLogger 5 | 6 | require 'forwardable' 7 | 8 | extend Forwardable 9 | 10 | def_delegators :@logger, :debug, :error, :info, :warn 11 | 12 | # For capistrano output 13 | # Only output Cap commands in debug mode 14 | def puts(msg, line_prefix=nil) 15 | @logger.debug msg.chomp 16 | end 17 | 18 | def initialize(args = {}) 19 | @log_level = args[:log_level] ||= 'info' 20 | @logger = args[:logger] ||= new_logger(args) 21 | end 22 | 23 | def logger_level 24 | Logger.const_get @log_level.upcase 25 | end 26 | 27 | # Added to support capistrano version 2.13.5 28 | def tty? 29 | nil 30 | end 31 | 32 | private 33 | 34 | def new_logger(args) 35 | Logger.new(STDOUT).tap do |l| 36 | l.datetime_format = '%Y-%m-%dT%H:%M:%S%z' 37 | l.formatter = proc do |severity, datetime, progname, msg| 38 | "#{datetime} #{severity} : #{msg}\n" 39 | end 40 | l.level = logger_level 41 | end 42 | end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/simple_deploy/misc.rb: -------------------------------------------------------------------------------- 1 | require 'simple_deploy/misc/attribute_merger' 2 | -------------------------------------------------------------------------------- /lib/simple_deploy/misc/attribute_merger.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | module Misc 3 | class AttributeMerger 4 | 5 | def merge(args) 6 | @attributes = args[:attributes] 7 | @config = SimpleDeploy.config 8 | @environment = args[:environment] 9 | @input_stacks = args[:input_stacks] 10 | @template = args[:template] 11 | 12 | combine_provided_and_mapped_attributes 13 | end 14 | 15 | private 16 | 17 | def combine_provided_and_mapped_attributes 18 | @attributes + mapped_attributes_not_provided 19 | end 20 | 21 | def mapped_attributes 22 | mapper.map_outputs_from_stacks :stacks => @input_stacks, 23 | :template => @template 24 | end 25 | 26 | def mapped_attributes_not_provided 27 | mapped_attributes.reject do |a| 28 | provided_attribute_keys.include? a.keys.first 29 | end 30 | end 31 | 32 | def provided_attribute_keys 33 | @attributes.map {|a| a.keys.first} 34 | end 35 | 36 | def mapper 37 | @om ||= Stack::OutputMapper.new :environment => @environment 38 | end 39 | 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/simple_deploy/notifier.rb: -------------------------------------------------------------------------------- 1 | require 'simple_deploy/notifier/campfire' 2 | require 'simple_deploy/notifier/slack' 3 | 4 | module SimpleDeploy 5 | class Notifier 6 | def initialize(args) 7 | @config = SimpleDeploy.config 8 | @stack_name = args[:stack_name] 9 | @environment = args[:environment] 10 | @notifications = @config.notifications || {} 11 | end 12 | 13 | def send_deployment_start_message 14 | message = "Deployment to #{@stack_name} in #{@config.region} started." 15 | send message 16 | end 17 | 18 | def send_deployment_complete_message 19 | message = "Deployment to #{@stack_name} in #{@config.region} complete." 20 | attributes = stack.attributes 21 | 22 | if attributes['app_github_url'] 23 | message += " App: #{attributes['app_github_url']}/commit/#{attributes['app']}" 24 | end 25 | 26 | if attributes['chef_repo_github_url'] 27 | message += " Chef: #{attributes['chef_repo_github_url']}/commit/#{attributes['chef_repo']}" 28 | end 29 | 30 | send message 31 | end 32 | 33 | def send(message) 34 | @notifications.keys.each do |notification| 35 | case notification 36 | when 'campfire' 37 | campfire = Notifier::Campfire.new :stack_name => @stack_name, 38 | :environment => @environment 39 | campfire.send message 40 | when 'slack' 41 | slack = Notifier::Slack.new 42 | slack.send message 43 | end 44 | end 45 | end 46 | 47 | private 48 | 49 | def stack 50 | @stack ||= Stack.new :name => @stack_name, 51 | :environment => @environment 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/simple_deploy/notifier/campfire.rb: -------------------------------------------------------------------------------- 1 | require 'esbit' 2 | 3 | module SimpleDeploy 4 | class Notifier 5 | class Campfire 6 | 7 | def initialize(args) 8 | @stack_name = args[:stack_name] 9 | @environment = args[:environment] 10 | @config = SimpleDeploy.config 11 | @logger = SimpleDeploy.logger 12 | 13 | attributes = stack.attributes 14 | @subdomain = attributes['campfire_subdomain'] 15 | @room_ids = attributes['campfire_room_ids'] ||= '' 16 | @logger.debug "Campfire subdomain '#{@subdomain}'." 17 | @logger.debug "Campfire room ids '#{@room_ids}'." 18 | 19 | if @subdomain 20 | @token = @config.notifications['campfire']['token'] 21 | @campfire = Esbit::Campfire.new @subdomain, @token 22 | @rooms = @campfire.rooms 23 | end 24 | end 25 | 26 | def send(message) 27 | @logger.info "Sending Campfire notifications." 28 | @room_ids.split(',').each do |room_id| 29 | room = @rooms.find { |r| r.id == room_id.to_i } 30 | if room 31 | @logger.debug "Sending notification to Campfire room #{room.name}." 32 | room.say message 33 | else 34 | @logger.warn "Could not find a room for id #{room_id}" 35 | end 36 | end 37 | @logger.info "Campfire notifications complete." 38 | end 39 | 40 | private 41 | 42 | def stack 43 | @stack ||= Stack.new :name => @stack_name, 44 | :environment => @environment 45 | end 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/simple_deploy/notifier/slack.rb: -------------------------------------------------------------------------------- 1 | require 'slack-notifier' 2 | 3 | module SimpleDeploy 4 | class Notifier 5 | class Slack 6 | 7 | def initialize(args = {}) 8 | @logger = SimpleDeploy.logger 9 | @notifier = ::Slack::Notifier.new SimpleDeploy.config.notifications['slack']['webhook_url'] 10 | end 11 | 12 | def send(message) 13 | @logger.info "Sending Slack notification." 14 | @notifier.ping message 15 | @logger.info "Slack notification complete." 16 | end 17 | 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack.rb: -------------------------------------------------------------------------------- 1 | require 'simple_deploy/stack/deployment' 2 | require 'simple_deploy/stack/execute' 3 | require 'simple_deploy/stack/output_mapper' 4 | require 'simple_deploy/stack/stack_attribute_formatter' 5 | require 'simple_deploy/stack/stack_creator' 6 | require 'simple_deploy/stack/stack_destroyer' 7 | require 'simple_deploy/stack/stack_formatter' 8 | require 'simple_deploy/stack/stack_lister' 9 | require 'simple_deploy/stack/stack_reader' 10 | require 'simple_deploy/stack/stack_updater' 11 | require 'simple_deploy/stack/status' 12 | 13 | module SimpleDeploy 14 | class Stack 15 | 16 | def initialize(args) 17 | @environment = args[:environment] 18 | @name = args[:name] 19 | 20 | @config = SimpleDeploy.config 21 | @logger = SimpleDeploy.logger 22 | 23 | @use_internal_ips = !!args[:internal] 24 | @use_external_ips = !!args[:external] 25 | @entry = Entry.new :name => @name 26 | end 27 | 28 | def create(args) 29 | attributes = stack_attribute_formatter.updated_attributes args[:attributes] 30 | @template_file = args[:template] 31 | 32 | @entry.set_attributes attributes 33 | stack_creator.create 34 | 35 | @entry.save 36 | end 37 | 38 | def update(args) 39 | if !deployment.clear_for_deployment? && args[:force] 40 | deployment.clear_deployment_lock true 41 | 42 | Backoff.exp_periods do |p| 43 | sleep p 44 | break if deployment.clear_for_deployment? 45 | end 46 | end 47 | 48 | if deployment.clear_for_deployment? 49 | @logger.info "Updating #{@name}." 50 | attributes = stack_attribute_formatter.updated_attributes args[:attributes] 51 | @template_body = args[:template_body] || template 52 | 53 | @entry.set_attributes attributes 54 | stack_updater.update_stack attributes 55 | @logger.info "Update complete for #{@name}." 56 | 57 | @entry.save 58 | true 59 | else 60 | @logger.info "Not clear to update." 61 | false 62 | end 63 | end 64 | 65 | def in_progress_update(args) 66 | if args[:caller].kind_of? Stack::Deployment::Status 67 | @logger.info "Updating #{@name}." 68 | attributes = stack_attribute_formatter.updated_attributes args[:attributes] 69 | @template_body = args[:template_body] || template 70 | 71 | @entry.set_attributes attributes 72 | stack_updater.update_stack attributes 73 | @logger.info "Update complete for #{@name}." 74 | 75 | @entry.save 76 | true 77 | else 78 | false 79 | end 80 | end 81 | 82 | def deploy(force = false) 83 | deployment.execute force 84 | end 85 | 86 | def execute(args) 87 | executer.execute args 88 | end 89 | 90 | def ssh 91 | deployment.ssh 92 | end 93 | 94 | def destroy 95 | unless exists? 96 | @logger.error "#{@name} does not exist" 97 | return false 98 | end 99 | 100 | if attributes['protection'] != 'on' 101 | stack_destroyer.destroy 102 | @entry.delete_attributes 103 | @logger.info "#{@name} destroyed." 104 | true 105 | else 106 | @logger.warn "#{@name} could not be destroyed because it is protected. Run the protect subcommand to unprotect it" 107 | false 108 | end 109 | end 110 | 111 | def events(limit) 112 | stack_reader.events limit 113 | end 114 | 115 | def outputs 116 | stack_reader.outputs 117 | end 118 | 119 | def resources 120 | stack_reader.resources 121 | end 122 | 123 | def instances 124 | stack_reader.instances.map do |instance| 125 | instance['instancesSet'].map do |info| 126 | determine_ip_address(info) 127 | end 128 | end.flatten.compact 129 | end 130 | 131 | def raw_instances 132 | stack_reader.instances 133 | end 134 | 135 | def status 136 | stack_reader.status 137 | end 138 | 139 | def wait_for_stable 140 | stack_status.wait_for_stable 141 | end 142 | 143 | def exists? 144 | status 145 | true 146 | rescue Exceptions::UnknownStack 147 | false 148 | end 149 | 150 | def attributes 151 | stack_reader.attributes 152 | end 153 | 154 | def parameters 155 | stack_reader.parameters 156 | end 157 | 158 | def template 159 | stack_reader.template 160 | end 161 | 162 | private 163 | 164 | def stack_creator 165 | @stack_creator ||= StackCreator.new :name => @name, 166 | :entry => @entry, 167 | :template_file => @template_file 168 | end 169 | 170 | def stack_updater 171 | @stack_updater ||= StackUpdater.new :name => @name, 172 | :entry => @entry, 173 | :template_body => @template_body 174 | end 175 | 176 | def stack_reader 177 | @stack_reader ||= StackReader.new :name => @name 178 | end 179 | 180 | def stack_destroyer 181 | @stack_destroyer ||= StackDestroyer.new :name => @name 182 | end 183 | 184 | def stack_status 185 | @status ||= Status.new :name => @name 186 | end 187 | 188 | def stack_attribute_formatter 189 | @saf ||= StackAttributeFormatter.new :main_attributes => attributes 190 | end 191 | 192 | def executer 193 | @executer ||= Stack::Execute.new :environment => @environment, 194 | :name => @name, 195 | :stack => self, 196 | :instances => instances, 197 | :ssh_user => ssh_user, 198 | :ssh_key => ssh_key 199 | end 200 | 201 | def deployment 202 | @deployment ||= Stack::Deployment.new :environment => @environment, 203 | :name => @name, 204 | :stack => self, 205 | :instances => instances, 206 | :ssh_user => ssh_user, 207 | :ssh_key => ssh_key 208 | end 209 | 210 | def determine_ip_address(info) 211 | if info['vpcId'] 212 | address = @use_external_ips ? info['ipAddress'] : info['privateIpAddress'] 213 | unless address 214 | @logger.warn "Instance '#{info['instanceId']}' does not have an external address, skipping." 215 | end 216 | address 217 | else 218 | @use_internal_ips ? info['privateIpAddress'] : info['ipAddress'] 219 | end 220 | end 221 | 222 | def ssh_key 223 | ENV['SIMPLE_DEPLOY_SSH_KEY'] ||= "#{ENV['HOME']}/.ssh/id_rsa" 224 | end 225 | 226 | def ssh_user 227 | ENV['SIMPLE_DEPLOY_SSH_USER'] ||= ENV['USER'] 228 | end 229 | 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/deployment.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano' 2 | require 'capistrano/cli' 3 | 4 | require 'simple_deploy/stack/deployment/status' 5 | require 'simple_deploy/stack/execute' 6 | 7 | module SimpleDeploy 8 | class Stack 9 | 10 | class Deployment 11 | 12 | def initialize(args) 13 | @config = SimpleDeploy.config 14 | @logger = SimpleDeploy.logger 15 | @region = @config.region 16 | @instances = args[:instances] 17 | @environment = args[:environment] 18 | @ssh_user = args[:ssh_user] 19 | @ssh_key = args[:ssh_key] 20 | @stack = args[:stack] 21 | @name = args[:name] 22 | end 23 | 24 | def execute(force=false) 25 | wait_for_clear_to_deploy(force) 26 | 27 | if clear_for_deployment? 28 | status.set_deployment_in_progress 29 | 30 | @logger.info 'Starting deployment.' 31 | return_val = executer.execute :sudo => true, 32 | :command => deploy_command 33 | 34 | return false unless return_val 35 | 36 | @logger.info 'Deployment complete.' 37 | 38 | status.unset_deployment_in_progress 39 | true 40 | else 41 | @logger.error "Not clear to deploy." 42 | false 43 | end 44 | end 45 | 46 | def clear_deployment_lock(force = false) 47 | status.clear_deployment_lock force 48 | end 49 | 50 | def clear_for_deployment? 51 | status.clear_for_deployment? 52 | end 53 | 54 | private 55 | 56 | def wait_for_clear_to_deploy(force) 57 | if !clear_for_deployment? && force 58 | clear_deployment_lock true 59 | 60 | Backoff.exp_periods do |p| 61 | sleep p 62 | break if clear_for_deployment? 63 | end 64 | end 65 | end 66 | 67 | def deploy_command 68 | cmd = 'env ' 69 | get_artifact_endpoints.each_pair do |key,value| 70 | cmd += "#{key}=#{value} " 71 | end 72 | cmd += "PRIMARY_HOST=#{primary_instance} #{deploy_script}" 73 | end 74 | 75 | def get_artifact_endpoints 76 | h = {} 77 | @config.artifacts.each do |artifact| 78 | variable = @config.artifact_deploy_variable artifact 79 | bucket_prefix = attributes["#{artifact}_bucket_prefix"] 80 | domain = attributes["#{artifact}_domain"] 81 | encrypted = attributes["#{artifact}_encrypted"] == 'true' 82 | 83 | artifact = Artifact.new :name => artifact, 84 | :id => attributes[artifact], 85 | :region => @region, 86 | :domain => domain, 87 | :bucket_prefix => bucket_prefix, 88 | :encrypted => encrypted 89 | 90 | h[variable] = artifact.endpoints['s3'] 91 | end 92 | h 93 | end 94 | 95 | def primary_instance 96 | if @stack.raw_instances.any? 97 | @stack.raw_instances.first['instancesSet'].first['privateIpAddress'] 98 | end 99 | end 100 | 101 | def deploy_script 102 | @config.deploy_script 103 | end 104 | 105 | def executer 106 | options = { :instances => @instances, 107 | :environment => @environment, 108 | :ssh_user => @ssh_user, 109 | :ssh_key => @ssh_key, 110 | :stack => @stack, 111 | :name => @name } 112 | @executer ||= SimpleDeploy::Stack::Execute.new options 113 | end 114 | 115 | def status 116 | options = { :name => @name, 117 | :environment => @environment, 118 | :stack => @stack, 119 | :ssh_user => @ssh_user } 120 | @status ||= SimpleDeploy::Stack::Deployment::Status.new options 121 | end 122 | 123 | def attributes 124 | @stack.attributes 125 | end 126 | 127 | end 128 | 129 | end 130 | end 131 | 132 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/deployment/status.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class Stack 3 | class Deployment 4 | class Status 5 | 6 | def initialize(args) 7 | @config = SimpleDeploy.config 8 | @logger = SimpleDeploy.logger 9 | @stack = args[:stack] 10 | @ssh_user = args[:ssh_user] 11 | @name = args[:name] 12 | end 13 | 14 | def clear_for_deployment? 15 | !deployment_in_progress? 16 | end 17 | 18 | def clear_deployment_lock(force=false) 19 | if deployment_in_progress? && force 20 | @logger.info "Forcing. Clearing deployment status." 21 | unset_deployment_in_progress 22 | end 23 | end 24 | 25 | def deployment_in_progress? 26 | @logger.debug "Checking deployment status for #{@name}." 27 | if attributes['deployment_in_progress'] == 'true' 28 | @logger.info "Deployment in progress for #{@name}." 29 | @logger.info "Started by #{attributes['deployment_user']}@#{attributes['deployment_datetime']}." 30 | true 31 | else 32 | @logger.debug "No other deployments in progress for #{@name}." 33 | false 34 | end 35 | end 36 | 37 | def set_deployment_in_progress 38 | @logger.debug "Setting deployment in progress by #{@ssh_user} for #{@name}." 39 | @stack.update :attributes => [ { 'deployment_in_progress' => 'true', 40 | 'deployment_user' => @ssh_user, 41 | 'deployment_datetime' => Time.now.to_s } ] 42 | end 43 | 44 | def unset_deployment_in_progress 45 | @logger.debug "Clearing deployment in progress for #{@name}." 46 | @stack.in_progress_update :attributes => [ 47 | { 'deployment_in_progress' => 'false' } ], 48 | :caller => self 49 | end 50 | 51 | private 52 | 53 | def attributes 54 | @stack.attributes 55 | end 56 | 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/execute.rb: -------------------------------------------------------------------------------- 1 | require 'simple_deploy/stack/ssh' 2 | 3 | module SimpleDeploy 4 | class Stack 5 | class Execute 6 | def initialize(args) 7 | @config = SimpleDeploy.config 8 | @args = args 9 | end 10 | 11 | def execute(args) 12 | ssh.execute args 13 | end 14 | 15 | private 16 | 17 | def ssh 18 | @ssh ||= SimpleDeploy::Stack::SSH.new @args 19 | end 20 | 21 | end 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/output_mapper.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class Stack 3 | class OutputMapper 4 | 5 | def initialize(args) 6 | @environment = args[:environment] 7 | @logger = SimpleDeploy.logger 8 | end 9 | 10 | def map_outputs_from_stacks(args) 11 | @stacks = args[:stacks] 12 | @template = args[:template] 13 | @results = {} 14 | 15 | merge_stacks_outputs 16 | 17 | pluralize_keys 18 | prune_unused_parameters 19 | 20 | @results.each_pair do |key, value| 21 | @logger.info "Mapping output '#{key}' to input parameter with value '#{value}'." 22 | end 23 | 24 | @results.map { |x| { x.first => x.last } } 25 | end 26 | 27 | private 28 | 29 | def merge_stacks_outputs 30 | count = 0 31 | 32 | @stacks.each do |s| 33 | count += 1 34 | @logger.info "Reading outputs from stack '#{s}'." 35 | stack = Stack.new :environment => @environment, 36 | :name => s 37 | stack.wait_for_stable 38 | merge_outputs stack 39 | SimpleDeploy::Backoff.exp_periods(count < 5 ? count : 5) do |backoff| 40 | @logger.info "Backing off for #{backoff} seconds." 41 | sleep backoff 42 | end 43 | end 44 | end 45 | 46 | def merge_outputs(stack) 47 | stack.outputs.each do |output| 48 | key = output['OutputKey'] 49 | value = output['OutputValue'] 50 | 51 | @logger.debug "Read output #{key}=#{value}." 52 | 53 | if @results.has_key? key 54 | @results[key] += ",#{value}" 55 | else 56 | @results[key] = value 57 | end 58 | end 59 | end 60 | 61 | def pluralize_keys 62 | plural_params = @results.each_with_object({}) do |results, new| 63 | key = results.first 64 | pluralized_key = "#{key}s" 65 | 66 | if template_parameters.include? pluralized_key 67 | new[pluralized_key] = results[1] 68 | end 69 | end 70 | 71 | @results.merge! plural_params 72 | end 73 | 74 | def prune_unused_parameters 75 | @results.select! { |key| template_parameters.include? key } 76 | end 77 | 78 | def template_parameters 79 | @parameters ||= Template.new(:file => @template).parameters 80 | end 81 | 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/ssh.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano' 2 | require 'capistrano/cli' 3 | 4 | module SimpleDeploy 5 | class Stack 6 | class SSH 7 | 8 | def initialize(args) 9 | @config = SimpleDeploy.config 10 | @logger = SimpleDeploy.logger 11 | @stack = args[:stack] 12 | @instances = args[:instances] 13 | @environment = args[:environment] 14 | @ssh_user = args[:ssh_user] 15 | @ssh_key = args[:ssh_key] 16 | @name = args[:name] 17 | @region = @config.region 18 | end 19 | 20 | def execute(args) 21 | return false if @instances.nil? || @instances.empty? 22 | create_execute_task args 23 | 24 | status = false 25 | 26 | begin 27 | @task.execute 28 | status = true 29 | @logger.info "Command executed against instances successfully." 30 | rescue ::Capistrano::CommandError => error 31 | @logger.error "Error running execute statement: #{error}" 32 | rescue ::Capistrano::ConnectionError => error 33 | @logger.error "Error connecting to instances: #{error}" 34 | rescue ::Capistrano::Error => error 35 | @logger.error "Error: #{error}" 36 | end 37 | 38 | status 39 | end 40 | 41 | private 42 | 43 | def create_execute_task(args) 44 | 45 | @task = Capistrano::Configuration.new :output => @logger 46 | @task.logger.level = 3 47 | 48 | set_ssh_gateway 49 | set_ssh_user 50 | set_ssh_options 51 | set_instances 52 | set_execute_command args 53 | end 54 | 55 | def set_execute_command(args) 56 | command = args[:command] 57 | sudo = args[:sudo] 58 | pty = args[:pty] 59 | 60 | if pty 61 | @logger.debug "Setting pty to true." 62 | @task.variables[:default_run_options] = { :pty => true } 63 | end 64 | 65 | @logger.info "Setting command: '#{command}'." 66 | if sudo 67 | @task.load :string => "task :execute do 68 | sudo '#{command}' 69 | end" 70 | else 71 | @task.load :string => "task :execute do 72 | run '#{command}' 73 | end" 74 | end 75 | end 76 | 77 | def set_instances 78 | @instances.each do |instance| 79 | @logger.debug "Executing command on instance #{instance}." 80 | @task.server instance, :instances 81 | end 82 | end 83 | 84 | def set_ssh_options 85 | @logger.debug "Setting key to #{@ssh_key}." 86 | @task.variables[:ssh_options] = { :keys => @ssh_key, 87 | :paranoid => false } 88 | end 89 | 90 | def set_ssh_gateway 91 | ssh_gateway = attributes['ssh_gateway'] 92 | if ssh_gateway && !ssh_gateway.empty? 93 | @task.set :gateway, ssh_gateway 94 | @logger.info "Proxying via gateway #{ssh_gateway}." 95 | else 96 | @logger.debug "Not using an ssh gateway." 97 | end 98 | end 99 | 100 | def set_ssh_user 101 | @logger.debug "Setting user to #{@ssh_user}." 102 | @task.set :user, @ssh_user 103 | end 104 | 105 | def attributes 106 | @stack.attributes 107 | end 108 | 109 | end 110 | end 111 | end 112 | 113 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/stack_attribute_formatter.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class StackAttributeFormatter 3 | 4 | def initialize(args) 5 | @config = SimpleDeploy.config 6 | @logger = SimpleDeploy.logger 7 | @main_attributes = args[:main_attributes] 8 | @region = @config.region 9 | end 10 | 11 | def updated_attributes(attributes) 12 | @provided_attributes = attributes 13 | 14 | updates = [] 15 | @provided_attributes.each do |attrhash| 16 | key = attrhash.keys.first 17 | if artifact_names.include? key 18 | url_hash = cloud_formation_url attrhash, @provided_attributes 19 | updates << url_hash 20 | @logger.info "Adding artifact attribute: #{url_hash}" 21 | end 22 | end 23 | @provided_attributes + updates 24 | end 25 | 26 | private 27 | 28 | def artifact_names 29 | @config.artifacts 30 | end 31 | 32 | def cloud_formation_url(selected_attribute, updated_attributes) 33 | name = selected_attribute.keys.first 34 | id = selected_attribute[name] 35 | 36 | bucket_prefix, domain = find_bucket_prefix_and_domain selected_attribute, updated_attributes 37 | 38 | artifact = Artifact.new :name => name, 39 | :id => id, 40 | :region => @region, 41 | :domain => domain, 42 | :encrypted => artifact_encrypted?(name), 43 | :bucket_prefix => bucket_prefix 44 | 45 | url_parameter = @config.artifact_cloud_formation_url name 46 | url_value = artifact.endpoints['s3'] 47 | 48 | { url_parameter => url_value } 49 | end 50 | 51 | def artifact_encrypted?(name) 52 | provided_attributes_encrypted = @provided_attributes.select do |attribute| 53 | attribute["#{name}_encrypted"] == 'true' 54 | end.any? 55 | main_attributes_encrypted = @main_attributes["#{name}_encrypted"] == 'true' 56 | 57 | provided_attributes_encrypted || main_attributes_encrypted 58 | end 59 | 60 | def find_bucket_prefix_and_domain(selected_attribute, updated_attributes) 61 | name = selected_attribute.keys.first 62 | 63 | bucket_match = updated_attributes.find { |h| h.has_key? "#{name}_bucket_prefix" } 64 | if bucket_match 65 | bucket_prefix = bucket_match["#{name}_bucket_prefix"] 66 | else 67 | bucket_prefix = @main_attributes["#{name}_bucket_prefix"] 68 | end 69 | 70 | domain_match = updated_attributes.find { |h| h.has_key? "#{name}_domain" } 71 | if domain_match 72 | domain = domain_match["#{name}_domain"] 73 | else 74 | domain = @main_attributes["#{name}_domain"] 75 | end 76 | 77 | [bucket_prefix, domain] 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/stack_creator.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module SimpleDeploy 4 | class StackCreator 5 | 6 | def initialize(args) 7 | @config = SimpleDeploy.config 8 | @logger = SimpleDeploy.logger 9 | @entry = args[:entry] 10 | @name = args[:name] 11 | @template = read_template_from_file args[:template_file] 12 | end 13 | 14 | def create 15 | @logger.info "Creating Cloud Formation stack #{@name}." 16 | cloud_formation.create :name => @name, 17 | :parameters => read_parameters_from_entry, 18 | :template => @template 19 | end 20 | 21 | private 22 | 23 | def cloud_formation 24 | @cf ||= AWS::CloudFormation.new 25 | end 26 | 27 | def read_template_from_file(template_file) 28 | file = File.open template_file 29 | file.read 30 | end 31 | 32 | def read_parameters_from_template 33 | t = JSON.parse @template 34 | t['Parameters'] ? t['Parameters'].keys : [] 35 | end 36 | 37 | def read_parameters_from_entry 38 | h = {} 39 | attributes = @entry.attributes 40 | read_parameters_from_template.each do |p| 41 | h[p] = attributes[p] if attributes[p] 42 | end 43 | h 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/stack_destroyer.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class StackDestroyer 3 | 4 | def initialize(args) 5 | @config = SimpleDeploy.config 6 | @name = args[:name] 7 | end 8 | 9 | def destroy 10 | cloud_formation.destroy @name 11 | end 12 | 13 | private 14 | 15 | def cloud_formation 16 | @cf ||= AWS::CloudFormation.new 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/stack_formatter.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class StackFormatter 3 | 4 | def initialize(args) 5 | @name = args[:name] 6 | @config = SimpleDeploy.config 7 | end 8 | 9 | def display 10 | { 11 | 'attributes' => stack_reader.attributes, 12 | 'status' => stack_reader.status, 13 | 'outputs' => stack_reader.outputs, 14 | 'events' => stack_reader.events(3), 15 | 'resources' => stack_reader.resources, 16 | } 17 | end 18 | 19 | private 20 | 21 | def stack_reader 22 | @stack_reader ||= StackReader.new :name => @name 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/stack_lister.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class StackLister 3 | 4 | def initialize(args = {}) 5 | @config = SimpleDeploy.config 6 | end 7 | 8 | def all 9 | entry_lister.all 10 | end 11 | 12 | private 13 | 14 | def entry_lister 15 | @entry_lister ||= EntryLister.new 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/stack_reader.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class StackReader 3 | 4 | def initialize(args) 5 | @name = args[:name] 6 | @config = SimpleDeploy.config 7 | end 8 | 9 | def attributes 10 | entry.attributes 11 | end 12 | 13 | def outputs 14 | cloud_formation.stack_outputs @name 15 | end 16 | 17 | def status 18 | cloud_formation.stack_status @name 19 | end 20 | 21 | def events(limit) 22 | cloud_formation.stack_events @name, limit 23 | end 24 | 25 | def resources 26 | cloud_formation.stack_resources @name 27 | end 28 | 29 | def template 30 | cloud_formation.template @name 31 | end 32 | 33 | def parameters 34 | json = JSON.parse template 35 | json['Parameters'].nil? ? [] : json['Parameters'].keys 36 | end 37 | 38 | def instances 39 | instance_reader.list_stack_instances @name 40 | end 41 | 42 | private 43 | 44 | def entry 45 | @entry ||= Entry.find :name => @name 46 | end 47 | 48 | def cloud_formation 49 | @cloud_formation ||= AWS::CloudFormation.new 50 | end 51 | 52 | def instance_reader 53 | @instance_reader ||= AWS::InstanceReader.new 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/stack_updater.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module SimpleDeploy 4 | class StackUpdater 5 | 6 | def initialize(args) 7 | @config = SimpleDeploy.config 8 | @logger = SimpleDeploy.logger 9 | @entry = args[:entry] 10 | @name = args[:name] 11 | @template_body = args[:template_body] 12 | end 13 | 14 | def update_stack(attributes) 15 | if parameter_updated?(attributes) || @template_body 16 | @logger.debug 'Updated parameters or new template found.' 17 | update 18 | else 19 | @logger.debug 'No parameters require updating and no new template found.' 20 | false 21 | end 22 | end 23 | 24 | private 25 | 26 | def update 27 | if status.wait_for_stable 28 | @logger.info "Updating Cloud Formation stack #{@name}." 29 | cloud_formation.update :name => @name, 30 | :parameters => read_parameters_from_entry_attributes, 31 | :template => @template_body 32 | else 33 | raise "#{@name} did not reach a stable state." 34 | end 35 | end 36 | 37 | def parameter_updated?(attributes) 38 | (template_parameters - updated_parameters(attributes)) != template_parameters 39 | end 40 | 41 | def template_parameters 42 | json = JSON.parse @template_body 43 | json['Parameters'].nil? ? [] : json['Parameters'].keys 44 | end 45 | 46 | def updated_parameters attributes 47 | (attributes.map { |s| s.keys }).flatten 48 | end 49 | 50 | def read_parameters_from_entry_attributes 51 | h = {} 52 | entry_attributes = @entry.attributes 53 | template_parameters.each do |p| 54 | if entry_attributes[p] == 'nil' 55 | @logger.debug "Skipping attribute #{p}" 56 | next 57 | end 58 | h[p] = entry_attributes[p] if entry_attributes[p] 59 | end 60 | h 61 | end 62 | 63 | def cloud_formation 64 | @cloud_formation ||= AWS::CloudFormation.new 65 | end 66 | 67 | def status 68 | @status ||= Status.new :name => @name 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/simple_deploy/stack/status.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class Status 3 | 4 | def initialize(args) 5 | @name = args[:name] 6 | @config = SimpleDeploy.config 7 | @logger = SimpleDeploy.logger 8 | end 9 | 10 | def complete? 11 | /_COMPLETE$/ === current 12 | end 13 | 14 | def failed? 15 | /_FAILED$/ === current 16 | end 17 | 18 | def cleanup_in_progress? 19 | /_CLEANUP_IN_PROGRESS$/ === current 20 | end 21 | 22 | def in_progress? 23 | /_IN_PROGRESS$/ === current && !cleanup_in_progress? 24 | end 25 | 26 | def create_failed? 27 | 'CREATE_FAILED' == current 28 | end 29 | 30 | def stable? 31 | (complete? || failed?) && (! create_failed?) 32 | end 33 | 34 | def wait_for_stable(count=25) 35 | 1.upto(count).each do |c| 36 | break if stable? 37 | @logger.info ("#{@name} not stable (#{current}). Sleeping #{c * c} second(s).") 38 | Kernel.sleep (c * c) 39 | end 40 | stable? 41 | end 42 | 43 | private 44 | 45 | def current 46 | stack_reader.status 47 | end 48 | 49 | def stack_reader 50 | @stack_reader ||= StackReader.new :name => @name 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/simple_deploy/template.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | class Template 3 | def initialize(args) 4 | @file = args[:file] 5 | end 6 | 7 | def parameters 8 | parsed_template_contents.fetch('Parameters', {}).keys 9 | end 10 | 11 | private 12 | 13 | def parsed_template_contents 14 | JSON.parse contents 15 | end 16 | 17 | def contents 18 | @contents ||= IO.read @file 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/simple_deploy/version.rb: -------------------------------------------------------------------------------- 1 | module SimpleDeploy 2 | VERSION = "0.10.2" 3 | end 4 | -------------------------------------------------------------------------------- /simple_deploy.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "simple_deploy/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "simple_deploy" 7 | s.version = SimpleDeploy::VERSION 8 | s.authors = ["Intuit Open Source"] 9 | s.email = ["CTO-DevOpenSource@intuit.com"] 10 | s.homepage = "" 11 | s.summary = %q{Opinionated gem for AWS resource management.} 12 | s.description = %q{Opinionated gem for Managing AWS Cloud Formation stacks and deploying updates to Instances.} 13 | 14 | s.rubyforge_project = "simple_deploy" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | 21 | s.add_development_dependency "fakefs", "~> 0.4.2" 22 | s.add_development_dependency "rake" 23 | s.add_development_dependency "rspec", "~> 2.13.0" 24 | s.add_development_dependency "simplecov", "~> 0.7.1" 25 | s.add_development_dependency "timecop", "~> 0.6.1" 26 | 27 | s.add_runtime_dependency "capistrano", "= 2.13.5" 28 | s.add_runtime_dependency "esbit", "~> 0.0.4" 29 | s.add_runtime_dependency "trollop", "= 2.0" 30 | s.add_runtime_dependency "fog", "= 1.23.0" 31 | s.add_runtime_dependency "retries", "= 0.0.5" 32 | s.add_runtime_dependency "slack-notifier", "= 1.2.0" 33 | end 34 | -------------------------------------------------------------------------------- /spec/artifact_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy do 4 | 5 | describe "an artifact" do 6 | 7 | context "when unencrypted" do 8 | before do 9 | @artifact = SimpleDeploy::Artifact.new :bucket_prefix => 'test_prefix', 10 | :domain => 'us-west-1', 11 | :id => 'abc123', 12 | :name => 'myapp', 13 | :region => 'us-west-1', 14 | :encrypted => false 15 | end 16 | 17 | it "should return the endpoints for the artifact" do 18 | endpoints = { "s3" => "s3://test_prefix-us-west-1/us-west-1/abc123.tar.gz", 19 | "http" => "http://s3-us-west-1.amazonaws.com/test_prefix-us-west-1/us-west-1/abc123.tar.gz", 20 | "https" => "https://s3-us-west-1.amazonaws.com/test_prefix-us-west-1/us-west-1/abc123.tar.gz" } 21 | @artifact.endpoints.should == endpoints 22 | end 23 | end 24 | 25 | context "when encrypted" do 26 | before do 27 | @artifact = SimpleDeploy::Artifact.new :bucket_prefix => 'test_prefix', 28 | :domain => 'us-west-1', 29 | :id => 'abc123', 30 | :name => 'myapp', 31 | :region => 'us-west-1', 32 | :encrypted => true 33 | end 34 | 35 | it "should return the endpoints for the artifact" do 36 | endpoints = { "s3" => "s3://test_prefix-us-west-1/us-west-1/abc123.tar.gz.gpg", 37 | "http" => "http://s3-us-west-1.amazonaws.com/test_prefix-us-west-1/us-west-1/abc123.tar.gz.gpg", 38 | "https" => "https://s3-us-west-1.amazonaws.com/test_prefix-us-west-1/us-west-1/abc123.tar.gz.gpg" } 39 | @artifact.endpoints.should == endpoints 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/aws/cloud_formation/error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::AWS::CloudFormation::Error do 4 | include_context 'double stubbed config', :access_key => 'key', 5 | :secret_key => 'XXX', 6 | :region => 'us-west-1' 7 | include_context 'double stubbed logger' 8 | 9 | before do 10 | @exception_stub1 = stub 'Fog::AWS::CloudFormation' 11 | @exception_stub1.stub(:message).and_return("No updates are to be performed.") 12 | 13 | @exception_stub2 = stub 'Fog::AWS::CloudFormation' 14 | @exception_stub2.stub(:message).and_return("Oops.") 15 | 16 | @exception_stub3 = stub 'Fog::AWS::CloudFormation' 17 | @exception_stub3.stub(:message).and_return("Stack:test does not exist") 18 | 19 | @exception_stub4 = stub 'Fog::AWS::CloudFormation::' 20 | @exception_stub4.stub(:message).and_return('') 21 | end 22 | 23 | describe 'process' do 24 | it 'should process no update messages' do 25 | error = SimpleDeploy::AWS::CloudFormation::Error.new :exception => @exception_stub1 26 | expect { error.process }.to_not raise_error SimpleDeploy::Exceptions::CloudFormationError 27 | end 28 | 29 | it 'should raise an error if the exception is blank' do 30 | error = SimpleDeploy::AWS::CloudFormation::Error.new :exception => @exception_stub4 31 | expect { error.process }.to raise_error SimpleDeploy::Exceptions::CloudFormationError 32 | end 33 | 34 | it 'should re-raise unknown errors as SimpleDeploy::CloudFormationError' do 35 | error = SimpleDeploy::AWS::CloudFormation::Error.new :exception => @exception_stub2 36 | 37 | lambda { error.process }.should raise_error SimpleDeploy::Exceptions::CloudFormationError 38 | end 39 | 40 | it 'should re-raise unkonwn errors as SimpleDeploy::CloudFormationError and set mesg' do 41 | error = SimpleDeploy::AWS::CloudFormation::Error.new :exception => @exception_stub2 42 | begin 43 | error.process 44 | rescue SimpleDeploy::Exceptions::CloudFormationError => e 45 | e.message.should == "Oops." 46 | end 47 | end 48 | 49 | it 'should reaise stck unkown messages as SimpleDeploy::UnknownStack' do 50 | error = SimpleDeploy::AWS::CloudFormation::Error.new :exception => @exception_stub3 51 | lambda { error.process }.should raise_error SimpleDeploy::Exceptions::UnknownStack 52 | end 53 | 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/aws/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class TestObj 4 | include SimpleDeploy::AWS::Helpers 5 | attr_accessor :config 6 | end 7 | 8 | describe SimpleDeploy::AWS::Helpers do 9 | 10 | describe 'connection_args' do 11 | before do 12 | @config = stub 'config', 13 | access_key: 'key', 14 | secret_key: 'XXX', 15 | region: 'us-west-1' 16 | @obj = TestObj.new 17 | 18 | @args = { 19 | aws_access_key_id: 'key', 20 | aws_secret_access_key: 'XXX', 21 | region: 'us-west-1' 22 | } 23 | end 24 | 25 | describe 'with long lived credentials' do 26 | before do 27 | @config.stub temporary_credentials?: false 28 | @obj.config = @config 29 | end 30 | 31 | it 'does not include security token' do 32 | @obj.connection_args.should eq @args 33 | end 34 | end 35 | 36 | describe 'with temporary credentials' do 37 | before do 38 | @config.stub security_token: 'token' 39 | @config.stub temporary_credentials?: true 40 | @obj.config = @config 41 | end 42 | 43 | it 'includes security security token' do 44 | args = @args.merge({aws_session_token: 'token'}) 45 | @obj.connection_args.should eq args 46 | end 47 | end 48 | 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /spec/aws/simpledb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::AWS::SimpleDB do 4 | include_context 'double stubbed config', :access_key => 'key', 5 | :secret_key => 'XXX', 6 | :region => 'us-west-1' 7 | 8 | before do 9 | @response_stub = stub 'Excon::Response', :body => { 10 | 'RequestId' => 'rid', 11 | 'Domains' => ['domain1', 'domain2'], 12 | 'Items' => { 'item1' => { 'key' => ['value'] } }, 13 | 'NextToken' => nil 14 | } 15 | @multi_response_stub = stub 'Excon::Response', :body => { 16 | 'RequestId' => 'rid', 17 | 'Domains' => ['domain1', 'domain2'], 18 | 'Items' => { 'item1-2' => { 'key' => ['value'] } }, 19 | 'NextToken' => 'Chunk2' 20 | } 21 | end 22 | 23 | describe 'temporary credentials' do 24 | include_context 'double stubbed config', :access_key => 'key', 25 | :secret_key => 'XXX', 26 | :security_token => 'the token', 27 | :temporary_credentials? => true, 28 | :region => 'us-west-1' 29 | 30 | it 'creates a connection with the temporary credentials' do 31 | args = { 32 | aws_access_key_id: 'key', 33 | aws_secret_access_key: 'XXX', 34 | aws_session_token: 'the token', 35 | region: 'us-west-1' 36 | } 37 | Fog::AWS::SimpleDB.should_receive(:new).with(args) 38 | SimpleDeploy::AWS::SimpleDB.new 39 | end 40 | end 41 | 42 | describe 'with long lived credentials' do 43 | include_context 'double stubbed config', :access_key => 'key', 44 | :secret_key => 'XXX', 45 | :security_token => nil, 46 | :temporary_credentials? => false, 47 | :region => 'us-west-1' 48 | before do 49 | @db_mock = mock 'SimpleDB' 50 | Fog::AWS::SimpleDB.stub(:new).and_return(@db_mock) 51 | @db_mock.stub(:list_domains).and_return(@response_stub) 52 | 53 | @db = SimpleDeploy::AWS::SimpleDB.new 54 | end 55 | 56 | describe 'domains' do 57 | it 'should return a list of domains' do 58 | @db.domains.should == ['domain1', 'domain2'] 59 | end 60 | end 61 | 62 | describe 'domain_exists?' do 63 | it 'should return true for existing domains' do 64 | @db.domain_exists?('domain1').should be_true 65 | end 66 | 67 | it 'should return false for non-existent domains' do 68 | @db.domain_exists?('baddomain1').should_not be_true 69 | end 70 | end 71 | 72 | describe 'create_domain' do 73 | it 'should create a new domain' do 74 | @db_mock.should_receive(:create_domain).with('newdomain').and_return(@response_stub) 75 | 76 | @db.create_domain('newdomain').body['RequestId'].should == 'rid' 77 | end 78 | 79 | it 'should not create a duplicate domain' do 80 | @db_mock.should_not_receive(:create_domain) 81 | 82 | @db.create_domain('domain1').should be_nil 83 | end 84 | end 85 | 86 | describe 'put_attributes' do 87 | it 'should update the specified domain' do 88 | @db_mock.should_receive(:put_attributes).with('domain1', 'item1', { 'key' => 'value' }, {}).and_return(@response_stub) 89 | 90 | @db.put_attributes('domain1', 'item1', { 'key' => 'value' }, {}).body['RequestId'].should == 'rid' 91 | end 92 | end 93 | 94 | describe 'select' do 95 | it 'should return query items' do 96 | @db_mock.should_receive(:select).with('item1', { "ConsistentRead" => true, "NextToken" => nil } ).and_return(@response_stub) 97 | 98 | @db.select('item1').should == { 'item1' => { 'key' => ['value'] } } 99 | end 100 | 101 | it 'should return multiple chunks of query items' do 102 | @db_mock.should_receive(:select).with('item1', { "ConsistentRead" => true, "NextToken" => nil } ).and_return(@multi_response_stub) 103 | @db_mock.should_receive(:select).with('item1', { "ConsistentRead" => true, "NextToken" => 'Chunk2' } ).and_return(@response_stub) 104 | 105 | @db.select('item1').should == { 'item1' => { 'key' => ['value'] }, 'item1-2' => { 'key' => ['value'] } } 106 | end 107 | end 108 | 109 | describe 'delete' do 110 | it 'should delete the attributes identified by domain and key' do 111 | @db_mock.should_receive(:delete_attributes).with('domain1', 'item1').and_return(@response_stub) 112 | 113 | @db.delete('domain1', 'item1').body['RequestId'].should == 'rid' 114 | end 115 | end 116 | 117 | describe 'delete_items' do 118 | it 'should delete the specific attributes passed associated with domain and key' do 119 | @db_mock.should_receive(:delete_attributes).with('domain1', 'item1', {'value'=>nil}).and_return(@response_stub) 120 | 121 | @db.delete_items('domain1', 'item1', {'value'=>nil}).body['RequestId'].should == 'rid' 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /spec/backoff_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Backoff do 4 | describe 'exp_periods' do 5 | it 'should yield each period' do 6 | expected_periods = [2, 4, 8] 7 | 8 | i = 0 9 | SimpleDeploy::Backoff.exp_periods do |p| 10 | expected_periods[i].should == p 11 | i += 1 12 | end 13 | end 14 | 15 | it 'should generate and yield a specified number of periods' do 16 | expected_periods = [2, 4] 17 | 18 | i = 0 19 | SimpleDeploy::Backoff.exp_periods(2) do |p| 20 | expected_periods[i].should == p 21 | i += 1 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/cli/attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'simple_deploy/cli' 3 | 4 | describe SimpleDeploy::CLI::Attributes do 5 | include_context 'cli config' 6 | include_context 'double stubbed logger' 7 | include_context 'double stubbed stack', :name => 'my_stack', 8 | :environment => 'my_env' 9 | 10 | describe 'with --read-from-env' do 11 | before do 12 | @options = { :environment => nil, 13 | :log_level => 'debug', 14 | :name => 'my_stack', 15 | :read_from_env => true } 16 | @stack_stub.stub(:attributes).and_return({ 'foo' => 'bar', 'baz' => 'blah' }) 17 | end 18 | 19 | it 'should output the attributes' do 20 | subject.should_receive(:valid_options?). 21 | with(:provided => @options, 22 | :required => [:environment, :name, :read_from_env]) 23 | Trollop.stub(:options).and_return(@options) 24 | subject.should_receive(:puts).with('foo: bar') 25 | subject.should_receive(:puts).with('baz: blah') 26 | subject.show 27 | end 28 | end 29 | 30 | describe 'show' do 31 | before do 32 | @options = { :environment => 'my_env', 33 | :log_level => 'debug', 34 | :name => 'my_stack' } 35 | @stack_stub.stub(:attributes).and_return({ 'foo' => 'bar', 'baz' => 'blah' }) 36 | end 37 | 38 | it 'should output the attributes' do 39 | subject.should_receive(:valid_options?). 40 | with(:provided => @options, 41 | :required => [:environment, :name, :read_from_env]) 42 | Trollop.stub(:options).and_return(@options) 43 | subject.should_receive(:puts).with('foo: bar') 44 | subject.should_receive(:puts).with('baz: blah') 45 | subject.show 46 | end 47 | 48 | context 'with --as-command-args' do 49 | before do 50 | @options[:as_command_args] = true 51 | Trollop.stub(:options).and_return(@options) 52 | subject.should_receive(:valid_options?). 53 | with(:provided => @options, 54 | :required => [:environment, :name, :read_from_env]) 55 | end 56 | 57 | it 'should output the attributes as command arguments' do 58 | subject.should_receive(:puts).with("-a baz=blah -a foo=bar") 59 | subject.show 60 | end 61 | end 62 | 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /spec/cli/create_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'simple_deploy/cli' 3 | 4 | describe SimpleDeploy::CLI::Create do 5 | include_context 'cli config' 6 | include_context 'double stubbed logger' 7 | include_context 'stubbed stack', :name => 'mytest', 8 | :environment => 'test' 9 | 10 | before do 11 | @config_env = mock 'environment config' 12 | @attribute_merger_mock = mock 'attribute merger' 13 | 14 | @options = { :attributes => [ 'attr1=val1' ], 15 | :input_stack => [ 'stack1' ], 16 | :environment => 'test', 17 | :name => 'mytest', 18 | :log_level => 'info', 19 | :template => '/tmp/test.json' } 20 | Trollop.stub :options => @options 21 | 22 | SimpleDeploy.stub(:environments).and_return(@config_env) 23 | @config_env.should_receive(:keys).and_return(['test']) 24 | 25 | SimpleDeploy::Misc::AttributeMerger.stub :new => @attribute_merger_mock 26 | 27 | merge_options = { :attributes => [ { "attr1" => "val1" } ], 28 | :environment => 'test', 29 | :template => '/tmp/test.json', 30 | :input_stacks => ["stack1"] } 31 | @attribute_merger_mock.should_receive(:merge).with(merge_options). 32 | and_return({ "attr1" => "val1", 33 | "attr2" => "val2" }) 34 | @create = SimpleDeploy::CLI::Create.new 35 | end 36 | 37 | it "should create a stack with provided and merged attributes" do 38 | @stack_mock.should_receive(:create). 39 | with({ :attributes => { "attr1" => "val1", 40 | "attr2" => "val2" }, 41 | :template => '/tmp/test.json' }) 42 | @create.create 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/cli/deploy_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | require 'simple_deploy/cli' 4 | 5 | describe SimpleDeploy::CLI::Deploy do 6 | include_context 'cli config' 7 | include_context 'double stubbed logger' 8 | include_context 'stubbed stack', :name => 'my_stack', 9 | :environment => 'my_env', 10 | :internal => false 11 | 12 | before { @required = [:environment, :name, :read_from_env] } 13 | 14 | describe 'deploy' do 15 | before do 16 | @stack_mock.stub(:attributes).and_return({}) 17 | @notifier = stub 18 | end 19 | 20 | it "should notify on success" do 21 | options = { :environment => 'my_env', 22 | :log_level => 'debug', 23 | :name => ['my_stack'], 24 | :force => true, 25 | :internal => false, 26 | :attributes => [] } 27 | 28 | subject.should_receive(:valid_options?). 29 | with(:provided => options, :required => @required) 30 | Trollop.stub(:options).and_return(options) 31 | 32 | SimpleDeploy::Notifier.should_receive(:new). 33 | with(:stack_name => 'my_stack', 34 | :environment => 'my_env'). 35 | and_return(@notifier) 36 | 37 | @stack_mock.should_receive(:wait_for_stable) 38 | @stack_mock.should_receive(:deploy).with(true).and_return(true) 39 | @notifier.should_receive(:send_deployment_start_message) 40 | @notifier.should_receive(:send_deployment_complete_message) 41 | 42 | subject.deploy 43 | end 44 | 45 | it "should exit on error with a status of 1" do 46 | options = { :environment => 'my_env', 47 | :log_level => 'debug', 48 | :name => ['my_stack'], 49 | :force => true, 50 | :external => false, 51 | :internal => false, 52 | :attributes => [] } 53 | 54 | subject.should_receive(:valid_options?). 55 | with(:provided => options, :required => @required) 56 | Trollop.stub(:options).and_return(options) 57 | 58 | SimpleDeploy::Notifier.should_receive(:new). 59 | with(:stack_name => 'my_stack', 60 | :environment => 'my_env'). 61 | and_return(@notifier) 62 | 63 | @stack_mock.should_receive(:wait_for_stable) 64 | @stack_mock.should_receive(:deploy).with(true).and_return(false) 65 | @notifier.should_receive(:send_deployment_start_message) 66 | 67 | begin 68 | subject.deploy 69 | rescue SystemExit => e 70 | e.status.should == 1 71 | end 72 | end 73 | 74 | it "should update the deploy attributes if any are passed" do 75 | options = { :environment => 'my_env', 76 | :log_level => 'debug', 77 | :name => ['my_stack'], 78 | :force => true, 79 | :external => false, 80 | :internal => false, 81 | :attributes => ['foo=bah'] } 82 | 83 | subject.should_receive(:valid_options?). 84 | with(:provided => options, :required => @required) 85 | Trollop.stub(:options).and_return(options) 86 | 87 | SimpleDeploy::Notifier.should_receive(:new). 88 | with(:stack_name => 'my_stack', 89 | :environment => 'my_env'). 90 | and_return(@notifier) 91 | 92 | @stack_mock.should_receive(:update).with(hash_including(:force => true, :attributes => [{'foo' => 'bah'}])).and_return(true) 93 | @stack_mock.should_receive(:wait_for_stable) 94 | @stack_mock.should_receive(:deploy).with(true).and_return(true) 95 | @notifier.should_receive(:send_deployment_start_message) 96 | @notifier.should_receive(:send_deployment_complete_message) 97 | 98 | subject.deploy 99 | end 100 | 101 | it "should exit with a status of 1 if the attributes update is not successful" do 102 | options = { :environment => 'my_env', 103 | :log_level => 'debug', 104 | :name => ['my_stack'], 105 | :force => true, 106 | :external => false, 107 | :internal => false, 108 | :attributes => ['foo=bah'] } 109 | 110 | subject.should_receive(:valid_options?). 111 | with(:provided => options, :required => @required) 112 | Trollop.stub(:options).and_return(options) 113 | 114 | SimpleDeploy::Notifier.should_receive(:new). 115 | with(:stack_name => 'my_stack', 116 | :environment => 'my_env'). 117 | and_return(@notifier) 118 | 119 | @stack_mock.should_receive(:update).with(hash_including(:force => true, 120 | :attributes => [{'foo' => 'bah'}])).and_return(false) 121 | @stack_mock.should_receive(:wait_for_stable) 122 | 123 | begin 124 | subject.deploy 125 | rescue SystemExit => e 126 | e.status.should == 1 127 | end 128 | end 129 | 130 | it "should do the deploy if there are no attributes to update" do 131 | options = { :environment => 'my_env', 132 | :log_level => 'debug', 133 | :name => ['my_stack'], 134 | :force => true, 135 | :external => false, 136 | :internal => false, 137 | :attributes => [] } 138 | 139 | subject.should_receive(:valid_options?). 140 | with(:provided => options, :required => @required) 141 | Trollop.stub(:options).and_return(options) 142 | 143 | SimpleDeploy::Notifier.should_receive(:new). 144 | with(:stack_name => 'my_stack', 145 | :environment => 'my_env'). 146 | and_return(@notifier) 147 | 148 | @stack_mock.should_not_receive(:update) 149 | @stack_mock.should_receive(:wait_for_stable) 150 | @stack_mock.should_receive(:deploy).with(true).and_return(true) 151 | @notifier.should_receive(:send_deployment_start_message) 152 | @notifier.should_receive(:send_deployment_complete_message) 153 | 154 | subject.deploy 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /spec/cli/destroy_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | require 'simple_deploy/cli' 4 | 5 | describe SimpleDeploy::CLI::Destroy do 6 | include_context 'cli config' 7 | include_context 'double stubbed logger' 8 | include_context 'stubbed stack', :name => 'my_stack', 9 | :environment => 'my_env' 10 | 11 | describe 'destroy' do 12 | before do 13 | @options = { :environment => 'my_env', 14 | :log_level => 'debug', 15 | :name => 'my_stack' } 16 | @required = [:environment, :name, :read_from_env] 17 | @stack_mock.stub(:attributes).and_return({}) 18 | end 19 | 20 | it "should exit with 0" do 21 | subject.should_receive(:valid_options?). 22 | with(:provided => @options, :required => @required) 23 | Trollop.stub(:options).and_return(@options) 24 | 25 | @stack_mock.should_receive(:destroy).and_return(true) 26 | 27 | begin 28 | subject.destroy 29 | rescue SystemExit => e 30 | e.status.should == 0 31 | end 32 | end 33 | 34 | it "should exit with 1" do 35 | subject.should_receive(:valid_options?). 36 | with(:provided => @options, :required => @required) 37 | Trollop.stub(:options).and_return(@options) 38 | 39 | @stack_mock.should_receive(:destroy).and_return(false) 40 | 41 | begin 42 | subject.destroy 43 | rescue SystemExit => e 44 | e.status.should == 1 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/cli/outputs_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'simple_deploy/cli' 3 | 4 | describe SimpleDeploy::CLI::Outputs do 5 | include_context 'cli config' 6 | include_context 'double stubbed logger' 7 | include_context 'stubbed stack', :name => 'mytest', 8 | :environment => 'test' 9 | 10 | before do 11 | @config_env = mock 'environment config' 12 | @options = { :environment => 'test', 13 | :log_level => 'info', 14 | :name => 'mytest' } 15 | @data = [{ 'OutputKey' => 'key1', 'OutputValue' => 'value1' }, 16 | { 'OutputKey' => 'key2', 'OutputValue' => 'value2' }] 17 | Trollop.stub :options => @options 18 | @config_mock.stub(:environments => { 'test' => 'data' }) 19 | SimpleDeploy.stub(:environments).and_return(@config_env) 20 | @config_env.should_receive(:keys).and_return(['test']) 21 | @stack_mock.stub(:outputs).and_return(@data) 22 | @outputs = SimpleDeploy::CLI::Outputs.new 23 | end 24 | 25 | after do 26 | SimpleDeploy.release_config 27 | end 28 | 29 | it "should successfully return the show command with default values" do 30 | @outputs.should_receive(:puts).with('key1: value1') 31 | @outputs.should_receive(:puts).with('key2: value2') 32 | @outputs.show 33 | end 34 | 35 | it "should successfully return the show command with as_command_args" do 36 | @options[:as_command_args] = true 37 | @outputs.should_receive(:print).with('-a key1=value1 ') 38 | @outputs.should_receive(:print).with('-a key2=value2 ') 39 | @outputs.should_receive(:puts).with('') 40 | @outputs.show 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /spec/cli/protect_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'simple_deploy/cli' 3 | 4 | describe SimpleDeploy::CLI::Protect do 5 | include_context 'cli config' 6 | include_context 'double stubbed logger' 7 | 8 | describe 'protect' do 9 | before do 10 | @options = { :environment => 'my_env', 11 | :log_level => 'debug', 12 | :name => ['my_stack1'], 13 | :protection => 'on' } 14 | @required = [:environment, :name, :read_from_env] 15 | end 16 | 17 | context "single stack" do 18 | include_context 'received stack array', 'my_stack', 'my_env', 1 19 | 20 | it "should enable protection" do 21 | subject.should_receive(:valid_options?). 22 | with(:provided => @options, :required => @required) 23 | Trollop.stub(:options).and_return(@options) 24 | 25 | @stack_mock1.stub(:attributes).and_return('protection' => 'on') 26 | @stack_mock1.should_receive(:update).with( 27 | hash_including(:attributes => [{ 'protection' => 'on' }])) 28 | 29 | subject.protect 30 | end 31 | 32 | it "should disable protection" do 33 | @options[:protection]= 'off' 34 | 35 | subject.should_receive(:valid_options?). 36 | with(:provided => @options, :required => @required) 37 | Trollop.stub(:options).and_return(@options) 38 | 39 | @stack_mock1.stub(:attributes).and_return('protection' => 'off') 40 | @stack_mock1.should_receive(:update).with( 41 | hash_including(:attributes => [{ 'protection' => 'off' }])) 42 | 43 | subject.protect 44 | end 45 | end 46 | 47 | context "multiple stacks" do 48 | include_context 'received stack array', 'my_stack', 'my_env', 2 49 | 50 | it "should enable protection" do 51 | @options[:name] = ['my_stack1', 'my_stack2'] 52 | 53 | subject.should_receive(:valid_options?). 54 | with(:provided => @options, :required => @required) 55 | Trollop.stub(:options).and_return(@options) 56 | 57 | @stack_mock1.stub(:attributes).and_return('protection' => 'on') 58 | @stack_mock1.should_receive(:update).with( 59 | hash_including(:attributes => [{ 'protection' => 'on' }])) 60 | @stack_mock2.stub(:attributes).and_return('protection' => 'on') 61 | @stack_mock2.should_receive(:update).with( 62 | hash_including(:attributes => [{ 'protection' => 'on' }])) 63 | 64 | subject.protect 65 | end 66 | 67 | it "should disable protection" do 68 | @options[:name] = ['my_stack1', 'my_stack2'] 69 | @options[:protection]= 'off' 70 | 71 | subject.should_receive(:valid_options?). 72 | with(:provided => @options, :required => @required) 73 | Trollop.stub(:options).and_return(@options) 74 | 75 | @stack_mock1.stub(:attributes).and_return('protection' => 'off') 76 | @stack_mock1.should_receive(:update).with( 77 | hash_including(:attributes => [{ 'protection' => 'off' }])) 78 | @stack_mock2.stub(:attributes).and_return('protection' => 'off') 79 | @stack_mock2.should_receive(:update).with( 80 | hash_including(:attributes => [{ 'protection' => 'off' }])) 81 | 82 | subject.protect 83 | end 84 | end 85 | 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/cli/shared_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'simple_deploy/cli' 3 | 4 | describe SimpleDeploy::CLI::Shared do 5 | include_context 'double stubbed logger' 6 | 7 | before do 8 | @object = Object.new 9 | @object.extend SimpleDeploy::CLI::Shared 10 | end 11 | 12 | it "should parse the given attributes" do 13 | logger_stub = stub 'logger stub', :info => true 14 | attributes = [ 'test1=value1', 'test2=value2==' ] 15 | 16 | @object.parse_attributes(:logger => @logger_stub, 17 | :attributes => attributes). 18 | should == [ { "test1" => "value1" }, 19 | { "test2" => "value2==" } ] 20 | end 21 | 22 | context "validating options " do 23 | describe 'when providing both environment and read_from_env' do 24 | before { @provided = { environment: 'env', read_from_env: true } } 25 | 26 | it 'exits' do 27 | lambda { 28 | @object.valid_options? provided: @provided, 29 | required: [:environment, :read_from_env] 30 | }.should raise_error SystemExit 31 | end 32 | end 33 | 34 | describe 'when either environment or read from env is required' do 35 | before { @required = [:environment, :read_from_env] } 36 | 37 | describe 'and neither is provided' do 38 | it 'exits' do 39 | lambda { 40 | @object.valid_options? provided: {}, required: @required 41 | }.should raise_error SystemExit 42 | end 43 | end 44 | 45 | describe 'and environment is provided' do 46 | describe 'and the environment exists' do 47 | 48 | it 'does not exit' do 49 | config_stub = stub 'config stub', 50 | environments: { 'prod' => 'data' }, 51 | keys: ['prod'] 52 | 53 | provided = { :environment => 'prod', :test1 => 'value1' } 54 | required = [:environment, :read_from_env, :test1] 55 | 56 | SimpleDeploy.stub(:environments).and_return(config_stub) 57 | 58 | @object.valid_options? :provided => provided, 59 | :required => required, 60 | :logger => @logger_stub 61 | end 62 | end 63 | 64 | describe 'and the environment does not exist' do 65 | 66 | it "exits" do 67 | config_stub = stub 'config stub', 68 | environments: { 'preprod' => 'data' }, 69 | keys: ['preprod'] 70 | 71 | provided = { :environment => 'prod' } 72 | required = [:environment, :read_from_env] 73 | 74 | SimpleDeploy.stub(:environments).and_return(config_stub) 75 | 76 | lambda { 77 | @object.valid_options? provided: provided, 78 | required: required 79 | }.should raise_error SystemExit 80 | end 81 | end 82 | end 83 | 84 | describe 'and read from env is provided' do 85 | describe 'and the env vars are set' do 86 | before do 87 | ENV['AWS_ACCESS_KEY_ID'] = 'access' 88 | ENV['AWS_SECRET_ACCESS_KEY'] = 'secret' 89 | ENV['AWS_REGION'] = 'us-west-1' 90 | end 91 | 92 | after do 93 | ENV['AWS_ACCESS_KEY_ID'] = nil 94 | ENV['AWS_SECRET_ACCESS_KEY'] = nil 95 | ENV['AWS_REGION'] = nil 96 | end 97 | 98 | it 'does not exit' do 99 | provided = { read_from_env: true, test1: 'value1' } 100 | required = [:environment, :read_from_env, :test1] 101 | 102 | @object.valid_options? :provided => provided, 103 | :required => required, 104 | :logger => @logger_stub 105 | end 106 | 107 | end 108 | 109 | describe 'and the env vars are not set' do 110 | 111 | it 'exits' do 112 | provided = { read_from_env: true } 113 | required = [:environment, :read_from_env] 114 | 115 | # SimpleDeploy.stub(:environments).and_return(config_stub) 116 | 117 | lambda { 118 | @object.valid_options? provided: provided, required: required 119 | }.should raise_error SystemExit 120 | end 121 | 122 | end 123 | 124 | end 125 | 126 | end 127 | 128 | it "should exit if provided options passed do not include all required" do 129 | provided = { :test1 => 'test1', :test2 => 'test2' } 130 | required = [:test1, :test2, :test3] 131 | 132 | lambda { 133 | @object.valid_options? :provided => provided, 134 | :required => required 135 | }.should raise_error SystemExit 136 | end 137 | 138 | it "should not exit if all options passed and environment exists" do 139 | config_stub = stub 'config stub', :environments => { 'prod' => 'data' } 140 | 141 | provided = { :environment => 'prod', :test1 => 'value1' } 142 | required = [:environment, :test1] 143 | 144 | SimpleDeploy.stub(:environments).and_return(config_stub) 145 | config_stub.should_receive(:keys).and_return(['prod']) 146 | 147 | @object.valid_options? :provided => provided, 148 | :required => required, 149 | :logger => @logger_stub 150 | end 151 | end 152 | 153 | it "should return the command name" do 154 | @object.command_name.should == 'object' 155 | end 156 | 157 | it "should rescue exceptions and exit 1" do 158 | lambda { @object.rescue_exceptions_and_exit do 159 | raise SimpleDeploy::Exceptions::Base 160 | end 161 | }.should raise_error SystemExit 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /spec/cli/update_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | require 'simple_deploy/cli' 4 | 5 | describe SimpleDeploy::CLI::Update do 6 | include_context 'cli config' 7 | include_context 'double stubbed logger' 8 | include_context 'received stack array', 'my_stack', 'my_env', 1 9 | 10 | describe 'update' do 11 | before do 12 | @stack_mock1.stub(:attributes).and_return({}) 13 | @template_body = "{ \"fake_json\" : \"goodness\"}" 14 | 15 | @options = { :environment => 'my_env', 16 | :log_level => 'debug', 17 | :name => ['my_stack1'], 18 | :force => true, 19 | :attributes => ['chef_repo_bucket_prefix=intu-lc'] } 20 | @required = [:environment, :name, :read_from_env] 21 | end 22 | 23 | it "should pass force true" do 24 | subject.should_receive(:valid_options?). 25 | with(:provided => @options, :required => @required) 26 | 27 | Trollop.stub(:options).and_return(@options) 28 | 29 | @stack_mock1.should_receive(:update).with(hash_including(:force => true)) 30 | 31 | subject.update 32 | end 33 | 34 | it "should pass force false" do 35 | @options[:force] = false 36 | 37 | subject.should_receive(:valid_options?). 38 | with(:provided => @options, :required => @required) 39 | 40 | Trollop.stub(:options).and_return(@options) 41 | 42 | @stack_mock1.should_receive(:update).with(hash_including(:force => false)) 43 | 44 | subject.update 45 | end 46 | 47 | it "should update the template if a new template is provided" do 48 | @options[:template] = 'brand_new_template.json' 49 | 50 | subject.should_receive(:valid_options?). 51 | with(:provided => @options, :required => @required) 52 | 53 | Trollop.stub(:options).and_return(@options) 54 | 55 | IO.should_receive(:read).with('brand_new_template.json').and_return(@template_body) 56 | 57 | @stack_mock1.should_receive(:update).with(hash_including(:template_body => @template_body)) 58 | subject.update 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'simple_deploy/cli' 3 | 4 | describe SimpleDeploy do 5 | 6 | it "should call the given sub command" do 7 | status_mock = mock 'status mock' 8 | ARGV.stub :shift => 'status' 9 | status_mock.should_receive(:show) 10 | SimpleDeploy::CLI::Status.stub :new => status_mock 11 | SimpleDeploy::CLI.start 12 | end 13 | 14 | describe 'environments' do 15 | let(:env) { mock('env').tap { |m| m.should_receive(:environments) } } 16 | 17 | before do 18 | ARGV.stub :shift => 'environments' 19 | SimpleDeploy::CLI::Environments.stub :new => env 20 | end 21 | 22 | it 'calls the correct command' do 23 | SimpleDeploy::CLI.start 24 | end 25 | 26 | context 'envs' do 27 | before { ARGV.stub :shift => 'envs'} 28 | 29 | it 'calls the correct command' do 30 | SimpleDeploy::CLI.start 31 | end 32 | end 33 | 34 | end 35 | 36 | end 37 | 38 | -------------------------------------------------------------------------------- /spec/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Configuration do 4 | let(:config_data) do 5 | { 'environments' => { 6 | 'test_env' => { 7 | 'access_key' => 'access', 8 | 'secret_key' => 'secret', 9 | 'security_token' => 'token', 10 | 'region' => 'us-west-1' 11 | } }, 12 | 'notifications' => { 13 | 'campfire' => { 14 | 'token' => 'my_token' 15 | } } } 16 | end 17 | 18 | describe 'creating a configuration' do 19 | before do 20 | @the_module = SimpleDeploy::Configuration 21 | end 22 | 23 | it 'should accept config data as an argument' do 24 | YAML.should_not_receive(:load) 25 | 26 | @config = @the_module.configure 'test_env', :config => config_data 27 | @config.environment.should == config_data['environments']['test_env'] 28 | @config.notifications.should == config_data['notifications'] 29 | end 30 | 31 | it 'should load the config from ~/.simple_deploy.yml by default' do 32 | File.should_receive(:open).with("#{ENV['HOME']}/.simple_deploy.yml"). 33 | and_return(config_data.to_yaml) 34 | 35 | @config = @the_module.configure 'test_env' 36 | @config.environment.should == config_data['environments']['test_env'] 37 | @config.notifications.should == config_data['notifications'] 38 | end 39 | 40 | it 'should load the config from SIMPLE_DEPLOY_CONFIG_FILE if supplied' do 41 | File.should_receive(:open).with("/my/config/file"). 42 | and_return(config_data.to_yaml) 43 | env_mock = mock 'env' 44 | @the_module.stub(:env).and_return(env_mock) 45 | env_mock.should_receive(:load). 46 | with('SIMPLE_DEPLOY_CONFIG_FILE'). 47 | and_return "/my/config/file" 48 | @config = @the_module.configure 'test_env' 49 | @config.environment.should == config_data['environments']['test_env'] 50 | @config.notifications.should == config_data['notifications'] 51 | end 52 | 53 | describe 'when the environment is :read_from_env' do 54 | before do 55 | ENV['AWS_ACCESS_KEY_ID'] = 'env_access' 56 | ENV['AWS_REGION'] = 'env_region' 57 | ENV['AWS_SECRET_ACCESS_KEY'] = 'env_secret' 58 | ENV['AWS_SECURITY_TOKEN'] = 'env_token' 59 | 60 | @data = { 61 | 'access_key' => 'env_access', 62 | 'region' => 'env_region', 63 | 'secret_key' => 'env_secret', 64 | 'security_token' => 'env_token' 65 | } 66 | end 67 | 68 | after do 69 | %w(ACCESS_KEY_ID REGION SECRET_ACCESS_KEY SECURITY_TOKEN).each do |i| 70 | ENV["AWS_#{i}"] = nil 71 | end 72 | end 73 | 74 | it 'loads the config from env vars' do 75 | @config = @the_module.configure :read_from_env 76 | @config.environment.should eq(@data) 77 | @config.notifications.should eq({}) 78 | end 79 | end 80 | 81 | end 82 | 83 | describe "after creating a configuration" do 84 | before do 85 | @the_module = SimpleDeploy::Configuration 86 | @config = @the_module.configure 'test_env', :config => config_data 87 | end 88 | 89 | it "should return the default artifacts to deploy" do 90 | @config.artifacts.should == ['chef_repo', 'cookbooks', 'app'] 91 | end 92 | 93 | it "should return the APP_URL for app" do 94 | @config.artifact_deploy_variable('app').should == 'APP_URL' 95 | end 96 | 97 | it "should return the Cloud Formation camel case variables" do 98 | @config.artifact_cloud_formation_url('app').should == 'AppArtifactURL' 99 | end 100 | 101 | it "should return the environment requested" do 102 | env_config = @config.environment 103 | env_config['access_key'].should == 'access' 104 | env_config['secret_key'].should == 'secret' 105 | env_config['security_token'].should == 'token' 106 | env_config['region'].should == 'us-west-1' 107 | end 108 | 109 | it "should return the notifications available" do 110 | @config.notifications.should == ( { 'campfire' => { 'token' => 'my_token' } } ) 111 | end 112 | 113 | it "should return the access_key for the environment" do 114 | @config.access_key.should == 'access' 115 | end 116 | 117 | it "should return the secret_key for the environment" do 118 | @config.secret_key.should == 'secret' 119 | end 120 | 121 | it "should return the security token for the environment" do 122 | @config.security_token.should == 'token' 123 | end 124 | 125 | it "should return the region for the environment" do 126 | @config.region.should == 'us-west-1' 127 | end 128 | 129 | it "should return the deploy script" do 130 | @config.deploy_script.should == '/opt/intu/admin/bin/configure.sh' 131 | end 132 | 133 | describe 'temporary_credentials?' do 134 | it 'is true when they are' do 135 | @config.temporary_credentials?.should be_true 136 | end 137 | 138 | describe 'when there is not a security token' do 139 | it 'is false when they are not' do 140 | config_data['environments']['test_env']['security_token'] = nil 141 | @config = @the_module.configure 'test_env', config: config_data 142 | @config.temporary_credentials?.should be_false 143 | end 144 | end 145 | end 146 | end 147 | 148 | describe 'showing raw configuration for all instances' do 149 | before do 150 | @the_module = SimpleDeploy::Configuration 151 | end 152 | 153 | it "should return a hash for every environment" do 154 | environments = @the_module.environments :config => config_data 155 | environments.keys.should == ['test_env'] 156 | end 157 | end 158 | 159 | describe "gracefully handling yaml file errors" do 160 | before do 161 | FakeFS.activate! 162 | @config_file_path = "#{ENV['HOME']}/.simple_deploy.yml" 163 | FileUtils.mkdir_p File.dirname(@config_file_path) 164 | 165 | @the_module = SimpleDeploy::Configuration 166 | end 167 | 168 | after do 169 | FakeFS.deactivate! 170 | FakeFS::FileSystem.clear 171 | end 172 | 173 | it "should handle a missing file gracefully" do 174 | expect { 175 | config = @the_module.configure 'test_env' 176 | }.to raise_error(RuntimeError, "#{@config_file_path} not found") 177 | end 178 | 179 | it "should handle a corrupt file gracefully" do 180 | s = "---\nport: | 80" 181 | File.open(@config_file_path, 'w') do |out| 182 | out.write(s) 183 | end 184 | 185 | expect { 186 | config = @the_module.configure 'test_env' 187 | }.to raise_error(RuntimeError, "#{@config_file_path} is corrupt") 188 | end 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /spec/contexts/config_contexts.rb: -------------------------------------------------------------------------------- 1 | shared_context "cli config" do 2 | before do 3 | @config_mock = mock 'config mock' 4 | SimpleDeploy.stub(:create_config).and_return(@config_mock) 5 | SimpleDeploy.stub(:config).and_return(@config_mock) 6 | end 7 | end 8 | 9 | shared_context "received config" do 10 | before do 11 | @config_mock = mock 'config mock' 12 | SimpleDeploy.should_receive(:config).and_return(@config_mock) 13 | end 14 | end 15 | 16 | shared_context "stubbed config" do 17 | before do 18 | @config_mock = mock 'config mock' 19 | SimpleDeploy.stub(:config).and_return(@config_mock) 20 | end 21 | end 22 | 23 | shared_context "double stubbed config" do |methods_hash| 24 | before do 25 | @config_stub = stub 'config stub', methods_hash 26 | SimpleDeploy.stub(:config).and_return(@config_stub) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/contexts/logger_contexts.rb: -------------------------------------------------------------------------------- 1 | shared_context "double stubbed logger" do 2 | before do 3 | @logger_stub = stub 'logger stub', :debug => true, 4 | :info => true, 5 | :warn => true, 6 | :error => true 7 | SimpleDeploy.stub(:logger).and_return(@logger_stub) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/contexts/stack_contexts.rb: -------------------------------------------------------------------------------- 1 | shared_context "stubbed stack" do |name, environment, options| 2 | before do 3 | args = { :name => name, :environment => environment } 4 | args[:use_internal_ips] = options[:internal] if options && options[:internal] 5 | @stack_mock = mock 'stack mock', args 6 | SimpleDeploy::Stack.stub(:new).and_return(@stack_mock) 7 | end 8 | end 9 | 10 | shared_context "double stubbed stack" do |name, environment, options| 11 | before do 12 | args = { :name => name, :environment => environment } 13 | args[:use_internal_ips] = options[:internal] if options && options[:internal] 14 | @stack_stub = stub 'stack stub', args 15 | SimpleDeploy::Stack.stub(:new).and_return(@stack_stub) 16 | end 17 | end 18 | 19 | shared_context "clone stack pair" do |source_name, source_env, new_name, new_env| 20 | before do 21 | @source_stack_stub = stub 'source stack stub', :name => source_name, 22 | :environment => source_env 23 | @new_stack_mock = mock 'new stack mock', :name => new_name, 24 | :environment => new_env 25 | SimpleDeploy::Stack.stub(:new).and_return(@source_stack_stub, @new_stack_mock) 26 | end 27 | end 28 | 29 | shared_context "received stack array" do |base_name, env, num_instances| 30 | before do 31 | 1.upto(num_instances) do |n| 32 | name = "#{base_name}#{n}" 33 | stack_mock = mock 'stack mock', :name => name, :environment => env 34 | self.instance_variable_set(:"@stack_mock#{n}", stack_mock) 35 | SimpleDeploy::Stack.should_receive(:new).with(:name => name, 36 | :environment => env). 37 | and_return(stack_mock) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/entry_lister_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::EntryLister do 4 | include_context 'stubbed config' 5 | 6 | it "should create a list of entries" do 7 | @simple_db_mock = mock 'simple db' 8 | SimpleDeploy::AWS::SimpleDB.should_receive(:new).and_return @simple_db_mock 9 | @simple_db_mock.should_receive(:domain_exists?). 10 | with("stacks"). 11 | and_return true 12 | @simple_db_mock.should_receive(:select). 13 | with("select * from stacks"). 14 | and_return('stack-to-find-us-west-1' => { 'attr1' => 'value1' }) 15 | entry_lister = SimpleDeploy::EntryLister.new 16 | entry_lister.all.should == ['stack-to-find'] 17 | end 18 | 19 | it "should return a blank array if the domain does not exist" do 20 | @simple_db_mock = mock 'simple db' 21 | SimpleDeploy::AWS::SimpleDB.should_receive(:new).and_return @simple_db_mock 22 | @simple_db_mock.should_receive(:domain_exists?). 23 | with("stacks"). 24 | and_return false 25 | @simple_db_mock.should_receive(:select). 26 | with("select * from stacks").exactly(0).times 27 | entry_lister = SimpleDeploy::EntryLister.new 28 | entry_lister.all.should == [] 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /spec/entry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Entry do 4 | include_context 'double stubbed logger' 5 | 6 | let(:config_data) do 7 | { 'environments' => { 8 | 'test_env' => { 9 | 'secret_key' => 'the-key', 10 | 'access_key' => 'access', 11 | 'region' => 'us-west-1' 12 | } } } 13 | end 14 | 15 | before do 16 | @config = SimpleDeploy.create_config 'test_env', :config => config_data 17 | end 18 | 19 | after do 20 | SimpleDeploy.release_config 21 | end 22 | 23 | it "should create a new entry object" do 24 | @simple_db_mock = mock 'simple db' 25 | SimpleDeploy::AWS::SimpleDB.should_receive(:new).and_return @simple_db_mock 26 | @simple_db_mock.should_receive(:create_domain). 27 | with("stacks"). 28 | and_return true 29 | entry = SimpleDeploy::Entry.new :name => 'test-stack' 30 | entry.class.should == SimpleDeploy::Entry 31 | end 32 | 33 | it "should find the requested stack in simple db" do 34 | @simple_db_mock = mock 'simple db' 35 | SimpleDeploy::AWS::SimpleDB.should_receive(:new).and_return @simple_db_mock 36 | 37 | @simple_db_mock.should_receive(:create_domain). 38 | with("stacks"). 39 | and_return true 40 | SimpleDeploy::Entry.find :name => 'stack-to-find' 41 | end 42 | 43 | context "with stack object" do 44 | before do 45 | @simple_db_mock = mock 'simple db' 46 | SimpleDeploy::AWS::SimpleDB.should_receive(:new).and_return @simple_db_mock 47 | @simple_db_mock.should_receive(:create_domain). 48 | with("stacks"). 49 | and_return true 50 | @entry = SimpleDeploy::Entry.new :name => 'test-stack' 51 | end 52 | 53 | it "should set the name to region-name for the stack" do 54 | @entry.name.should == 'test-stack-us-west-1' 55 | end 56 | 57 | it "should set the attributes in simple db including default attributes" do 58 | Timecop.travel Time.utc(2012, 10, 22, 13, 30) 59 | 60 | @simple_db_mock.should_receive(:select). 61 | with("select * from stacks where itemName() = 'test-stack-us-west-1'"). 62 | and_return('test-stack-us-west-1' => { 'key1' => ['value1'] }) 63 | @simple_db_mock.should_receive(:put_attributes). 64 | with("stacks", 65 | "test-stack-us-west-1", 66 | { "key" => "value", 67 | "key1" => "value1", 68 | "Name" => "test-stack-us-west-1", 69 | "CreatedAt" => "2012-10-22 13:30:00 UTC" }, 70 | { :replace => ["key1", "key", "Name", "CreatedAt"] } ) 71 | @entry.set_attributes(['key' => 'value']) 72 | 73 | @entry.save 74 | end 75 | 76 | it "should remove attributes set to nil" do 77 | Timecop.travel Time.utc(2012, 10, 22, 13, 30) 78 | 79 | @simple_db_mock.should_receive(:select). 80 | with("select * from stacks where itemName() = 'test-stack-us-west-1'"). 81 | and_return('test-stack-us-west-1' => { 'key1' => ['value1'] }) 82 | 83 | @simple_db_mock.should_receive(:delete_items). 84 | with("stacks", 85 | "test-stack-us-west-1", 86 | {'key2' => nil}) 87 | 88 | @simple_db_mock.should_receive(:delete_items). 89 | with("stacks", 90 | "test-stack-us-west-1", 91 | {'key3' => nil}) 92 | 93 | 94 | @simple_db_mock.should_receive(:put_attributes). 95 | with("stacks", 96 | "test-stack-us-west-1", 97 | { "key" => "value", 98 | "key1" => "value1", 99 | "Name" => "test-stack-us-west-1", 100 | "CreatedAt" => "2012-10-22 13:30:00 UTC" }, 101 | { :replace => ["key1", "key", "Name", "CreatedAt"] } ) 102 | @entry.set_attributes(['key' => 'value', 'key2' => 'nil', 'key3' => 'nil']) 103 | 104 | @entry.save 105 | end 106 | 107 | it "should merge custom attributes" do 108 | @simple_db_mock.should_receive(:select). 109 | with("select * from stacks where itemName() = 'test-stack-us-west-1'"). 110 | and_return('test-stack' => { 'key1' => ['value1'] }) 111 | @entry.set_attributes(['key1' => 'value2']) 112 | 113 | @entry.attributes.should == {'key1' => 'value2' } 114 | end 115 | end 116 | 117 | end 118 | -------------------------------------------------------------------------------- /spec/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy do 4 | 5 | before do 6 | @logger_mock = mock 'logger' 7 | end 8 | 9 | context "with new logger" do 10 | before do 11 | @logger_mock.should_receive(:datetime_format=).with '%Y-%m-%dT%H:%M:%S%z' 12 | @logger_mock.should_receive(:formatter=) 13 | @logger_mock.should_receive(:level=).with 1 14 | Logger.should_receive(:new).with(STDOUT).and_return @logger_mock 15 | end 16 | 17 | it "should create a new logger object when one is not passed" do 18 | @logger = SimpleDeploy::SimpleDeployLogger.new 19 | @logger_mock.should_receive(:info).with 'a message' 20 | @logger.info 'a message' 21 | end 22 | 23 | it "accept puts with msg and pass it to debug" do 24 | @logger = SimpleDeploy::SimpleDeployLogger.new 25 | @logger_mock.should_receive(:debug).with 'a message' 26 | @logger.puts 'a message' 27 | end 28 | 29 | it "tty? return false" do 30 | @logger = SimpleDeploy::SimpleDeployLogger.new 31 | @logger.tty?.should be_false 32 | end 33 | end 34 | 35 | it "should create a new logger object from the hash passed as :logger" do 36 | Logger.should_receive(:new).exactly(0).times 37 | @logger = SimpleDeploy::SimpleDeployLogger.new :logger => @logger_mock 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /spec/misc/attribute_merger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Misc::AttributeMerger do 4 | include_context 'stubbed config' 5 | 6 | before do 7 | @mapper_mock = mock 'mapper' 8 | 9 | @stacks = ['stack1', 'stack2'] 10 | @options = { :environment => 'default', 11 | :attributes => [ { 'attrib1' => 'val1' } ], 12 | :input_stacks => @stacks, 13 | :template => '/tmp/file.json' } 14 | SimpleDeploy::Stack::OutputMapper.should_receive(:new). 15 | with(:environment => @options[:environment]). 16 | and_return @mapper_mock 17 | @merger = SimpleDeploy::Misc::AttributeMerger.new 18 | end 19 | 20 | it "should return the consolidated list of attributes" do 21 | @mapper_mock.should_receive(:map_outputs_from_stacks). 22 | with(:stacks => @options[:input_stacks], 23 | :template => @options[:template]). 24 | and_return [ { 'attrib2' => 'val2' } ] 25 | @merger.merge(@options).should == [ { 'attrib1' => 'val1' }, 26 | { 'attrib2' => 'val2' } ] 27 | end 28 | 29 | it "should return provided attributes over outputs" do 30 | @mapper_mock.should_receive(:map_outputs_from_stacks). 31 | with(:stacks => @options[:input_stacks], 32 | :template => @options[:template]). 33 | and_return [ { 'attrib1' => 'val2' } ] 34 | @merger.merge(@options).should == [ { 'attrib1' => 'val1' } ] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/notifier/campfire_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Notifier::Campfire do 4 | include_context 'stubbed config' 5 | include_context 'double stubbed logger' 6 | include_context 'stubbed stack', :name => 'my_stack', 7 | :environment => 'my_env' 8 | 9 | before do 10 | @comms_mock = mock 'Campfire communications' 11 | 12 | @room1_mock = mock 'Esbit room1', :id => 1, :name => 'Room 1' 13 | @room2_mock = mock 'Esbit room2', :id => 2, :name => 'Room 2' 14 | @comms_mock.stub(:rooms).and_return([@room1_mock, @room2_mock]) 15 | end 16 | 17 | describe "with all required configurations" do 18 | before do 19 | config = { 'campfire' => { 'token' => 'tkn' } } 20 | @config_mock.should_receive(:notifications).and_return config 21 | 22 | Esbit::Campfire.should_receive(:new).with("subdom", "tkn"). 23 | and_return @comms_mock 24 | @stack_mock.should_receive(:attributes). 25 | and_return( 'campfire_room_ids' => '1,2', 26 | 'campfire_subdomain' => 'subdom' ) 27 | @campfire = SimpleDeploy::Notifier::Campfire.new :stack_name => 'stack_name', 28 | :environment => 'test' 29 | 30 | end 31 | 32 | it "should send a message to campfire rooms" do 33 | 34 | @room1_mock.should_receive(:say).with :message => "heh you guys!" 35 | @room2_mock.should_receive(:say).with :message => "heh you guys!" 36 | 37 | @campfire.send(:message => 'heh you guys!') 38 | end 39 | end 40 | 41 | describe "without valid attributes" do 42 | before do 43 | config = nil 44 | 45 | @stack_mock.should_receive(:attributes). 46 | and_return({}) 47 | @campfire = SimpleDeploy::Notifier::Campfire.new :stack_name => 'stack_name', 48 | :environment => 'test' 49 | end 50 | 51 | it "should not blow up if campfire_subdom & campfire_room_ids are not present" do 52 | @campfire.send(:message => 'heh you guys!') 53 | end 54 | end 55 | 56 | end 57 | 58 | -------------------------------------------------------------------------------- /spec/notifier/slack_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Notifier::Slack do 4 | include_context 'stubbed config' 5 | include_context 'double stubbed logger' 6 | 7 | before do 8 | @notifier = double('slack notifier') 9 | ::Slack::Notifier.stub(:new => @notifier) 10 | @config_mock.stub(:notifications => { 'slack' => { 'webhook_url' => 'url' } }) 11 | @slack = SimpleDeploy::Notifier::Slack.new 12 | end 13 | 14 | it 'should send a message to slack' do 15 | @notifier.should_receive(:ping).with('message') 16 | @slack.send('message') 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/notifier_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Notifier do 4 | include_context 'stubbed config' 5 | include_context 'stubbed stack', :name => 'stack_name', 6 | :environment => 'test' 7 | 8 | describe "with valid settings" do 9 | before do 10 | @config_mock.should_receive(:notifications). 11 | exactly(1).times. 12 | and_return({ 'campfire' => 'settings' }) 13 | @notifier = SimpleDeploy::Notifier.new :stack_name => 'stack_name', 14 | :environment => 'test' 15 | end 16 | 17 | after do 18 | SimpleDeploy.release_config 19 | end 20 | 21 | it "should support a basic start message" do 22 | campfire_mock = mock 'campfire mock' 23 | 24 | @config_mock.stub(:region).and_return('us-west-1') 25 | 26 | SimpleDeploy::Notifier::Campfire.should_receive(:new).and_return campfire_mock 27 | campfire_mock.should_receive(:send).with "Deployment to stack_name in us-west-1 started." 28 | 29 | @notifier.send_deployment_start_message 30 | end 31 | 32 | it "should include the github app & chef links in the completed message" do 33 | campfire_mock = mock 'campfire mock' 34 | environment_mock = mock 'environment mock' 35 | @config_mock.stub(:region).and_return('us-west-1') 36 | @stack_mock.should_receive(:attributes). 37 | and_return({ 'app_github_url' => 'http://github.com/user/app', 38 | 'chef_repo_github_url' => 'http://github.com/user/chef_repo', 39 | 'app' => 'appsha', 40 | 'chef_repo' => 'chefsha' }) 41 | SimpleDeploy::Notifier::Campfire.should_receive(:new). 42 | and_return campfire_mock 43 | campfire_mock.should_receive(:send). 44 | with "Deployment to stack_name in us-west-1 complete. App: http://github.com/user/app/commit/appsha Chef: http://github.com/user/chef_repo/commit/chefsha" 45 | @notifier.send_deployment_complete_message 46 | end 47 | 48 | it "should send a message to each listed notification endpoint" do 49 | campfire_mock = mock 'campfire mock' 50 | SimpleDeploy::Notifier::Campfire.should_receive(:new). 51 | with(:environment => 'test', 52 | :stack_name => 'stack_name'). 53 | and_return campfire_mock 54 | campfire_mock.should_receive(:send).with 'heh you guys!' 55 | @notifier.send 'heh you guys!' 56 | end 57 | 58 | end 59 | 60 | it "should not blow up if the notification section is missing" do 61 | @config_mock.should_receive(:notifications). 62 | and_return nil 63 | @notifier = SimpleDeploy::Notifier.new :stack_name => 'stack_name', 64 | :environment => 'test' 65 | @notifier.send 'heh you guys!' 66 | end 67 | 68 | end 69 | 70 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'fakefs/safe' 4 | require 'timecop' 5 | 6 | require 'simplecov' 7 | SimpleCov.start do 8 | add_filter "/spec/" 9 | end 10 | 11 | require 'simple_deploy' 12 | 13 | ['contexts'].each do |dir| 14 | Dir[File.expand_path(File.join(File.dirname(__FILE__),dir,'*.rb'))].each {|f| require f} 15 | end 16 | 17 | RSpec.configure do |config| 18 | #spec config 19 | end 20 | -------------------------------------------------------------------------------- /spec/stack/deployment/status_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Stack::Deployment::Status do 4 | include_context 'stubbed config' 5 | include_context 'double stubbed logger' 6 | include_context 'stubbed stack', :name => 'my_stack', 7 | :environment => 'my_env' 8 | 9 | before do 10 | options = { :stack => @stack_mock, 11 | :ssh_user => 'user', 12 | :name => 'dastack' } 13 | 14 | @status = SimpleDeploy::Stack::Deployment::Status.new options 15 | end 16 | 17 | describe "clear_for_deployment?" do 18 | it "should return true if clear for deployment" do 19 | @stack_mock.stub :attributes => { 'deployment_in_progress' => 'false' } 20 | @status.clear_for_deployment?.should be_true 21 | end 22 | 23 | it "should return false if not clear for deployment" do 24 | @stack_mock.stub :attributes => { 'deployment_in_progress' => 'true' } 25 | @status.clear_for_deployment?.should be_false 26 | end 27 | end 28 | 29 | describe "deployment_in_progress?" do 30 | it "should return false if no deployment in progress" do 31 | @stack_mock.stub :attributes => { 'deployment_in_progress' => 'false' } 32 | @status.deployment_in_progress?.should be_false 33 | end 34 | 35 | it "should return true if deployment in progress" do 36 | @stack_mock.stub :attributes => { 'deployment_in_progress' => 'true' } 37 | @status.deployment_in_progress?.should be_true 38 | end 39 | end 40 | 41 | describe "clear_deployment_lock" do 42 | it "should unset deploy in progress if force & deploy in progress" do 43 | @stack_mock.stub :attributes => { 'deployment_in_progress' => 'true' } 44 | @stack_mock.should_receive(:in_progress_update). 45 | with( { :attributes => [ { 'deployment_in_progress' => 'false' } ], 46 | :caller => @status }) 47 | @status.clear_deployment_lock(true) 48 | end 49 | end 50 | 51 | describe "set_deployment_in_prgoress" do 52 | it "set deployment as in progress" do 53 | Time.stub :now => 'thetime' 54 | @stack_mock.should_receive(:update). 55 | with( { :attributes => [ { "deployment_in_progress" => "true", "deployment_user" => "user", "deployment_datetime" => "thetime" } ] }) 56 | @status.set_deployment_in_progress 57 | end 58 | end 59 | 60 | describe "unset_deployment_in_prgoress" do 61 | it "clears deployment in progress" do 62 | @stack_mock.should_receive(:in_progress_update). 63 | with( { :attributes => [ { 'deployment_in_progress' => 'false'} ], 64 | :caller => @status }) 65 | @status.unset_deployment_in_progress 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /spec/stack/execute_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Stack::Execute do 4 | include_context 'stubbed config' 5 | include_context 'double stubbed stack', :name => 'my_stack', 6 | :environment => 'my_env' 7 | 8 | before do 9 | @ssh_mock = mock 'ssh' 10 | options = { :instances => @instances, 11 | :environment => @environment, 12 | :ssh_user => @ssh_user, 13 | :ssh_key => @ssh_key, 14 | :stack => @stack_stub, 15 | :name => @name } 16 | 17 | SimpleDeploy::Stack::SSH.should_receive(:new). 18 | with(options). 19 | and_return @ssh_mock 20 | @execute = SimpleDeploy::Stack::Execute.new options 21 | end 22 | 23 | it "should call execute with the given options" do 24 | options = { :sudo => true, :command => 'uname' } 25 | @ssh_mock.should_receive(:execute). 26 | with(options). 27 | and_return true 28 | @execute.execute(options).should be_true 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/stack/output_mapper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Stack::OutputMapper do 4 | include_context 'double stubbed logger' 5 | 6 | before do 7 | @config_mock = mock 'config' 8 | 9 | stack1_outputs = [ { 'OutputKey' => 'Test1', 'OutputValue' => 'val1' }, 10 | { 'OutputKey' => 'Nother', 'OutputValue' => 'another' } ] 11 | 12 | stack2_outputs = [ { 'OutputKey' => 'Test2', 'OutputValue' => 'val2' }, 13 | { 'OutputKey' => 'NotMe', 'OutputValue' => 'another' } ] 14 | 15 | stack3_outputs = [ { 'OutputKey' => 'Test1', 'OutputValue' => 'valA' } ] 16 | 17 | stack4_outputs = [ { 'OutputKey' => 'Test', 'OutputValue' => 'val' } ] 18 | 19 | @stack1_stub = stub 'stack1', :outputs => stack1_outputs, :wait_for_stable => true 20 | @stack2_stub = stub 'stack2', :outputs => stack2_outputs, :wait_for_stable => true 21 | @stack3_stub = stub 'stack3', :outputs => stack3_outputs, :wait_for_stable => true 22 | @stack4_stub = stub 'stack4', :outputs => stack4_outputs, :wait_for_stable => true 23 | 24 | @template_stub = stub 'template', :parameters => ["Test1", "Test2", "Tests"] 25 | 26 | @mapper = SimpleDeploy::Stack::OutputMapper.new :config => @config_mock, 27 | :environment => 'default' 28 | end 29 | 30 | context "when provided stacks" do 31 | before do 32 | SimpleDeploy::Template.should_receive(:new). 33 | with(:file => '/tmp/file.json'). 34 | and_return @template_stub 35 | end 36 | 37 | it "should return the outputs which match parameters" do 38 | SimpleDeploy::Stack.should_receive(:new). 39 | with(:environment => 'default', 40 | :name => 'stack1'). 41 | and_return @stack1_stub 42 | @mapper.should_receive(:sleep) 43 | @mapper.map_outputs_from_stacks(:stacks => ['stack1'], 44 | :template => '/tmp/file.json'). 45 | should == [{ 'Test1' => 'val1' }] 46 | end 47 | 48 | it "should return the outputs which match pluralized parameters" do 49 | SimpleDeploy::Stack.should_receive(:new). 50 | with(:environment => 'default', 51 | :name => 'stack4'). 52 | and_return @stack4_stub 53 | @mapper.should_receive(:sleep) 54 | @mapper.map_outputs_from_stacks(:stacks => ['stack4'], 55 | :template => '/tmp/file.json'). 56 | should == [{ 'Tests' => 'val' }] 57 | end 58 | 59 | it "should return the outputs which match parameters from multiple stacks" do 60 | SimpleDeploy::Stack.should_receive(:new). 61 | with(:environment => 'default', 62 | :name => 'stack1'). 63 | and_return @stack1_stub 64 | SimpleDeploy::Stack.should_receive(:new). 65 | with(:environment => 'default', 66 | :name => 'stack2'). 67 | and_return @stack2_stub 68 | @mapper.should_receive(:sleep).exactly(3).times 69 | @mapper.map_outputs_from_stacks(:stacks => ['stack1', 'stack2'], 70 | :template => '/tmp/file.json'). 71 | should == [{ 'Test1' => 'val1' }, {'Test2' => 'val2' }] 72 | end 73 | 74 | it "should concatenate multiple outputs of same name into CSV" do 75 | SimpleDeploy::Stack.should_receive(:new). 76 | with(:environment => 'default', 77 | :name => 'stack1'). 78 | and_return @stack1_stub 79 | SimpleDeploy::Stack.should_receive(:new). 80 | with(:environment => 'default', 81 | :name => 'stack3'). 82 | and_return @stack3_stub 83 | @mapper.should_receive(:sleep).exactly(3).times 84 | @mapper.map_outputs_from_stacks(:stacks => ['stack1', 'stack3'], 85 | :template => '/tmp/file.json'). 86 | should == [{ 'Test1' => 'val1,valA' }] 87 | end 88 | end 89 | 90 | it "should return an empty hash if no stacks are specified" do 91 | @mapper.map_outputs_from_stacks(:stacks => [], 92 | :template => '/tmp/file.json'). 93 | should == [] 94 | end 95 | 96 | end 97 | -------------------------------------------------------------------------------- /spec/stack/ssh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Stack::SSH do 4 | include_context 'stubbed config' 5 | include_context 'double stubbed logger' 6 | include_context 'double stubbed stack', :name => 'my_stack', 7 | :environment => 'my_env' 8 | 9 | before do 10 | @task_mock = mock 'task' 11 | @config_mock.should_receive(:region).and_return 'test-us-west-1' 12 | 13 | @stack_stub.stub(:attributes).and_return({ :ssh_gateway => false }) 14 | @options = { :instances => ['1.2.3.4', '4.3.2.1'], 15 | :environment => 'test-env', 16 | :ssh_user => 'user', 17 | :ssh_key => 'key', 18 | :stack => @stack_stub, 19 | :pty => true, 20 | :name => 'test-stack' } 21 | @task_logger_mock = mock 'task_logger' 22 | @ssh_options = Hash.new 23 | @task_mock.stub :logger => @task_logger_mock, 24 | :variables => @ssh_options 25 | end 26 | 27 | after do 28 | SimpleDeploy.release_config 29 | end 30 | 31 | context "when unsuccessful" do 32 | it "should return false when no running instances running" do 33 | @ssh = SimpleDeploy::Stack::SSH.new @options.merge({ :instances => [] }) 34 | 35 | @ssh.execute(:sudo => true, :command => 'uname').should be_false 36 | end 37 | 38 | context "with capistrano configured" do 39 | before do 40 | Capistrano::Configuration.should_receive(:new). 41 | with(:output => @logger_stub). 42 | and_return @task_mock 43 | 44 | @task_logger_mock.should_receive(:level=).with(3) 45 | @task_mock.should_receive(:set).with :user, 'user' 46 | @task_mock.should_receive(:server).with('1.2.3.4', :instances) 47 | @task_mock.should_receive(:server).with('4.3.2.1', :instances) 48 | end 49 | 50 | it "should return false when Capistrano command error" do 51 | @ssh = SimpleDeploy::Stack::SSH.new @options 52 | 53 | @task_mock.should_receive(:load).with({ :string=>"task :execute do\n sudo 'a_bad_command'\n end" }) 54 | @task_mock.should_receive(:execute).and_raise Capistrano::CommandError.new 'command error' 55 | 56 | @ssh.execute(:sudo => true, :command => 'a_bad_command').should be_false 57 | end 58 | 59 | it "should return false when Capistrano connection error" do 60 | @ssh = SimpleDeploy::Stack::SSH.new @options 61 | 62 | @task_mock.stub :logger => @task_logger_mock, 63 | :variables => @ssh_options 64 | @task_mock.should_receive(:load).with({ :string=>"task :execute do\n sudo 'uname'\n end" }) 65 | @task_mock.should_receive(:execute).and_raise Capistrano::ConnectionError.new 'connection error' 66 | 67 | @ssh.execute(:sudo => true, :command => 'uname').should be_false 68 | end 69 | 70 | it "should return false when Capistrano generic error" do 71 | @ssh = SimpleDeploy::Stack::SSH.new @options 72 | 73 | @task_mock.should_receive(:load).with({ :string=>"task :execute do\n sudo 'uname'\n end" }) 74 | @task_mock.should_receive(:execute).and_raise Capistrano::Error.new 'generic error' 75 | 76 | @ssh.execute(:sudo => true, :command => 'uname').should be_false 77 | end 78 | end 79 | end 80 | 81 | context "when successful" do 82 | before do 83 | @ssh = SimpleDeploy::Stack::SSH.new @options 84 | end 85 | 86 | describe "when execute called" do 87 | before do 88 | Capistrano::Configuration.should_receive(:new). 89 | with(:output => @logger_stub). 90 | and_return @task_mock 91 | 92 | @task_logger_mock.should_receive(:level=).with(3) 93 | @task_mock.should_receive(:set).with :user, 'user' 94 | @task_mock.should_receive(:server).with('1.2.3.4', :instances) 95 | @task_mock.should_receive(:server).with('4.3.2.1', :instances) 96 | end 97 | 98 | describe "when successful" do 99 | it "should execute a task with sudo" do 100 | @task_mock.should_receive(:load).with({ :string=>"task :execute do\n sudo 'uname'\n end" }) 101 | @task_mock.should_receive(:execute).and_return true 102 | 103 | @ssh.execute(:pty => false, 104 | :sudo => true, 105 | :command => 'uname').should be_true 106 | end 107 | 108 | it "should execute a task as the calling user " do 109 | @task_mock.should_receive(:load).with({ :string=>"task :execute do\n run 'uname'\n end" }) 110 | @task_mock.should_receive(:execute).and_return true 111 | 112 | @ssh.execute(:pty => false, 113 | :sudo => false, 114 | :command => 'uname').should be_true 115 | end 116 | 117 | it "should set the task variables" do 118 | @task_mock.should_receive(:load).with({ :string=>"task :execute do\n run 'uname'\n end" }) 119 | @task_mock.should_receive(:execute).and_return true 120 | 121 | @ssh.execute(:pty => false, 122 | :sudo => false, 123 | :command => 'uname') 124 | expect(@task_mock.variables).to eq ({ :ssh_options => { :keys => "key", :paranoid => false } }) 125 | end 126 | 127 | it "should set the pty to true" do 128 | @task_mock.should_receive(:load).with({ :string=>"task :execute do\n sudo 'uname'\n end" }) 129 | @task_mock.should_receive(:execute).and_return true 130 | 131 | @ssh.execute(:pty => true, 132 | :sudo => true, 133 | :command => 'uname') 134 | expect(@task_mock.variables[:default_run_options]).to eq ({ :pty => true }) 135 | end 136 | 137 | it "sets the ssh options" do 138 | @task_mock.stub(:load) 139 | @task_mock.stub(:execute).and_return(true) 140 | @ssh.execute :sudo => false, :command => 'uname' 141 | 142 | @ssh_options.should == { :ssh_options => { :keys => 'key', 143 | :paranoid => false } } 144 | end 145 | end 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /spec/stack/stack_attribute_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::StackAttributeFormatter do 4 | include_context 'stubbed config' 5 | include_context 'double stubbed logger' 6 | 7 | before do 8 | @config_mock.stub(:region).and_return('us-west-1') 9 | @config_mock.stub(:artifact_cloud_formation_url).and_return('ChefRepoURL') 10 | @config_mock.stub(:artifacts).and_return(['chef_repo', 'cookbooks', 'app']) 11 | end 12 | 13 | after do 14 | SimpleDeploy.release_config 15 | end 16 | 17 | context "when chef_repo unencrypted" do 18 | before do 19 | options = { :environment => 'preprod', 20 | :main_attributes => { 21 | 'chef_repo_bucket_prefix' => 'test-prefix', 22 | 'chef_repo_domain' => 'test-domain' } 23 | } 24 | @formatter = SimpleDeploy::StackAttributeFormatter.new options 25 | end 26 | 27 | it 'should return updated attributes including the un encrypted cloud formation url' do 28 | updates = @formatter.updated_attributes([ { 'chef_repo' => 'test123' } ]) 29 | updates.should == [{ 'chef_repo' => 'test123' }, 30 | { 'ChefRepoURL' => 's3://test-prefix-us-west-1/test-domain/test123.tar.gz' }] 31 | end 32 | end 33 | 34 | context "when main_attributes set chef_repo encrypted" do 35 | before do 36 | options = { :environment => 'preprod', 37 | :main_attributes => { 38 | 'chef_repo_bucket_prefix' => 'test-prefix', 39 | 'chef_repo_encrypted' => 'true', 40 | 'chef_repo_domain' => 'test-domain' } 41 | } 42 | @formatter = SimpleDeploy::StackAttributeFormatter.new options 43 | end 44 | 45 | it 'should return updated attributes including the encrypted cloud formation url ' do 46 | updates = @formatter.updated_attributes([ { 'chef_repo' => 'test123' } ]) 47 | updates.should == [{ 'chef_repo' => 'test123' }, 48 | { 'ChefRepoURL' => 's3://test-prefix-us-west-1/test-domain/test123.tar.gz.gpg' }] 49 | end 50 | end 51 | 52 | context "when provided attributes set chef_repo encrypted" do 53 | before do 54 | options = { :environment => 'preprod', 55 | :main_attributes => { 56 | 'chef_repo_bucket_prefix' => 'test-prefix', 57 | 'chef_repo_domain' => 'test-domain' } 58 | } 59 | @formatter = SimpleDeploy::StackAttributeFormatter.new options 60 | end 61 | 62 | it 'should return updated attributes including the encrypted cloud formation url ' do 63 | updates = @formatter.updated_attributes([ { 'chef_repo' => 'test123' }, 64 | { 'chef_repo_encrypted' => 'true' } ]) 65 | updates.should == [{ 'chef_repo' => 'test123' }, 66 | { 'chef_repo_encrypted' => 'true' }, 67 | { 'ChefRepoURL' => 's3://test-prefix-us-west-1/test-domain/test123.tar.gz.gpg' }] 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/stack/stack_creator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'json' 3 | 4 | describe SimpleDeploy::StackCreator do 5 | include_context 'stubbed config' 6 | include_context 'double stubbed logger' 7 | 8 | before do 9 | @attributes = { "param1" => "value1", "param3" => "value3" } 10 | @template_json = '{ "Parameters": 11 | { 12 | "param1" : 13 | { 14 | "Description" : "param-1" 15 | }, 16 | "param2" : 17 | { 18 | "Description" : "param-2" 19 | } 20 | } 21 | }' 22 | end 23 | 24 | it "should map the attributes to a template's parameters and create a stack " do 25 | entry_mock = mock 'entry mock' 26 | file_mock = mock 'file mock' 27 | cloud_formation_mock = mock 'cloud formation mock' 28 | 29 | SimpleDeploy::AWS::CloudFormation.should_receive(:new). 30 | and_return cloud_formation_mock 31 | File.should_receive(:open).with('path_to_file'). 32 | and_return file_mock 33 | file_mock.should_receive(:read).and_return @template_json 34 | entry_mock.should_receive(:attributes).and_return @attributes 35 | cloud_formation_mock.should_receive(:create). 36 | with(:name => 'test-stack', 37 | :parameters => { 'param1' => 'value1' }, 38 | :template => @template_json) 39 | stack_creator = SimpleDeploy::StackCreator.new :name => 'test-stack', 40 | :template_file => 'path_to_file', 41 | :entry => entry_mock 42 | 43 | stack_creator.create 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /spec/stack/stack_destroyer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::StackDestroyer do 4 | include_context 'stubbed config' 5 | include_context 'double stubbed logger' 6 | 7 | it "should destroy the stack" do 8 | cloud_formation_mock = mock 'cloud formation mock' 9 | 10 | SimpleDeploy::AWS::CloudFormation.should_receive(:new). 11 | and_return cloud_formation_mock 12 | cloud_formation_mock.should_receive(:destroy).with 'test-stack' 13 | 14 | stack_destroyer = SimpleDeploy::StackDestroyer.new :name => 'test-stack' 15 | stack_destroyer.destroy 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /spec/stack/stack_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ::SimpleDeploy::StackFormatter do 4 | include_context 'double stubbed config', :access_key => 'key', 5 | :secret_key => 'XXX', 6 | :region => 'us-west-1' 7 | 8 | before do 9 | @logger_stub = stub 'logger stub', :info => 'true', :warn => 'true' 10 | 11 | @stack_reader_mock = mock 'StackReader' 12 | SimpleDeploy::StackReader.stub(:new).and_return(@stack_reader_mock) 13 | @stack_reader_mock.stub(:attributes).and_return(:chef_repo_bucket_prefix => 'chef_repo_bp') 14 | @stack_reader_mock.stub(:outputs).and_return([{'key' => 'value'}]) 15 | @stack_reader_mock.stub(:status).and_return('green') 16 | @stack_reader_mock.stub(:events).and_return(['event1', 'event2', 'event3']) 17 | @stack_reader_mock.stub(:resources).and_return([{'StackName' => 'my_stack'}]) 18 | 19 | @stack_formatter = SimpleDeploy::StackFormatter.new(:name => 'my_stack') 20 | end 21 | 22 | after do 23 | SimpleDeploy.release_config 24 | end 25 | 26 | describe 'display' do 27 | it 'should return formatted information for the stack' do 28 | @stack_formatter.display.should == { 29 | 'attributes' => { :chef_repo_bucket_prefix => 'chef_repo_bp' }, 30 | 'status' => 'green', 31 | 'outputs' => [{'key' => 'value'}], 32 | 'events' => ['event1', 'event2', 'event3'], 33 | 'resources' => [{'StackName' => 'my_stack'}] 34 | } 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/stack/stack_lister_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::StackLister do 4 | include_context 'stubbed config' 5 | 6 | it "should list the stack entries" do 7 | entry_lister_mock = mock 'entry lister mock' 8 | 9 | SimpleDeploy::EntryLister.should_receive(:new). 10 | and_return entry_lister_mock 11 | entry_lister_mock.should_receive(:all) 12 | 13 | stack_lister = SimpleDeploy::StackLister.new 14 | stack_lister.all 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /spec/stack/stack_reader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::StackReader do 4 | include_context 'double stubbed config', :access_key => 'key', 5 | :secret_key => 'XXX', 6 | :region => 'us-west-1' 7 | 8 | before do 9 | @logger_stub = stub 'logger stub', :info => 'true', :warn => 'true' 10 | 11 | @entry_mock = mock 'Entry' 12 | @entry_mock.stub(:attributes).and_return(:chef_repo_bucket_prefix => 'chef_repo_bp') 13 | SimpleDeploy::Entry.stub(:new).and_return(@entry_mock) 14 | 15 | @cf_mock = mock 'CloudFormation' 16 | SimpleDeploy::AWS::CloudFormation.stub(:new).and_return(@cf_mock) 17 | @cf_mock.stub(:stack_outputs).and_return([{'key' => 'value'}]) 18 | @cf_mock.stub(:stack_status).and_return('green') 19 | @cf_mock.stub(:stack_events).and_return(['event1', 'event2']) 20 | @cf_mock.stub(:stack_resources).and_return([{'StackName' => 'my_stack'}]) 21 | @cf_mock.stub(:template).and_return('{"Parameters": {"EIP": "string"}}') 22 | 23 | @instance_reader_mock = mock 'InstanceReader' 24 | SimpleDeploy::AWS::InstanceReader.stub(:new).and_return(@instance_reader_mock) 25 | @instance_reader_mock.stub(:list_stack_instances).and_return(['instance1', 'instance2']) 26 | 27 | @stack_reader = SimpleDeploy::StackReader.new(:name => 'my_stack', :logger => @logger_stub) 28 | end 29 | 30 | after do 31 | SimpleDeploy.release_config 32 | end 33 | 34 | describe 'attributes' do 35 | it 'should return the stack attributes' do 36 | @stack_reader.attributes.should == { :chef_repo_bucket_prefix => 'chef_repo_bp' } 37 | end 38 | end 39 | 40 | describe 'outputs' do 41 | it 'should return the stack outputs' do 42 | @stack_reader.outputs.should == [{'key' => 'value'}] 43 | end 44 | end 45 | 46 | describe 'status' do 47 | it 'should return the stack status' do 48 | @stack_reader.status.should == 'green' 49 | end 50 | end 51 | 52 | describe 'events' do 53 | it 'should return the stack events' do 54 | @stack_reader.events(2).should == ['event1', 'event2'] 55 | end 56 | end 57 | 58 | describe 'resources' do 59 | it 'should return the stack resources' do 60 | @stack_reader.resources.should == [{'StackName' => 'my_stack'}] 61 | end 62 | end 63 | 64 | describe 'template' do 65 | it 'should return the stack template' do 66 | @stack_reader.template.should == '{"Parameters": {"EIP": "string"}}' 67 | end 68 | end 69 | 70 | describe 'parameters' do 71 | it 'should return the stack parameters' do 72 | @stack_reader.parameters.should == ['EIP'] 73 | end 74 | end 75 | 76 | describe 'instances' do 77 | it 'should return the stack instances' do 78 | @stack_reader.instances.should == ['instance1', 'instance2'] 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/stack/stack_updater_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'json' 3 | 4 | describe SimpleDeploy::StackUpdater do 5 | include_context 'stubbed config' 6 | include_context 'double stubbed logger' 7 | 8 | before do 9 | @template_body = '{ "Parameters": 10 | { 11 | "param1" : 12 | { 13 | "Description" : "param-1" 14 | }, 15 | "param2" : 16 | { 17 | "Description" : "param-2" 18 | } 19 | } 20 | }' 21 | 22 | @new_template_body = '{ "Parameters": 23 | { 24 | "param1" : 25 | { 26 | "Description" : "param-1" 27 | }, 28 | "param2" : 29 | { 30 | "Description" : "param-2" 31 | }, 32 | "param3" : 33 | { 34 | "Description" : "param-3" 35 | } 36 | } 37 | }' 38 | end 39 | 40 | it "should update the stack when parameters change and stack is stable" do 41 | attributes = { "param1" => "value1", "param3" => "value3" , "paramremove" => "nil" } 42 | entry_mock = mock 'entry mock' 43 | status_mock = mock 'status mock' 44 | cloud_formation_mock = mock 'cloud formation mock' 45 | SimpleDeploy::AWS::CloudFormation.should_receive(:new). 46 | and_return cloud_formation_mock 47 | entry_mock.should_receive(:attributes).and_return attributes 48 | cloud_formation_mock.should_receive(:update). 49 | with(:name => 'test-stack', 50 | :parameters => { 'param1' => 'value1' }, 51 | :template => @template_body). 52 | and_return true 53 | SimpleDeploy::Status.should_receive(:new). 54 | with(:name => 'test-stack'). 55 | and_return status_mock 56 | status_mock.should_receive(:wait_for_stable).and_return true 57 | stack_updater = SimpleDeploy::StackUpdater.new :name => 'test-stack', 58 | :template_body => @template_body, 59 | :entry => entry_mock 60 | 61 | stack_updater.update_stack( [ { 'param1' => 'new-value' } ] ). 62 | should == true 63 | end 64 | 65 | it "should update the stack when only the template body changes and stack is stable" do 66 | attributes = { "param1" => "value1", "param3" => "value3" , "paramremove" => "nil" } 67 | entry_mock = mock 'entry mock' 68 | status_mock = mock 'status mock' 69 | cloud_formation_mock = mock 'cloud formation mock' 70 | SimpleDeploy::AWS::CloudFormation.should_receive(:new). 71 | and_return cloud_formation_mock 72 | entry_mock.stub(:attributes).and_return attributes 73 | 74 | cloud_formation_mock.should_receive(:update). 75 | with(:name => 'test-stack', 76 | :parameters => { 'param1' => 'value1', 'param3' => 'value3' }, 77 | :template => @new_template_body). 78 | and_return true 79 | SimpleDeploy::Status.should_receive(:new). 80 | with(:name => 'test-stack'). 81 | and_return status_mock 82 | status_mock.stub(:wait_for_stable).and_return true 83 | stack_updater = SimpleDeploy::StackUpdater.new :name => 'test-stack', 84 | :template_body => @new_template_body, 85 | :entry => entry_mock 86 | 87 | stack_updater.update_stack([]).should == true 88 | end 89 | 90 | it "should raise an error when parameters change and stack is not stable" do 91 | attributes = { "param1" => "value1", "param3" => "value3" } 92 | entry_mock = mock 'entry mock' 93 | status_mock = mock 'status mock' 94 | cloud_formation_mock = mock 'cloud formation mock' 95 | SimpleDeploy::AWS::CloudFormation.should_receive(:new).never 96 | SimpleDeploy::Status.should_receive(:new). 97 | with(:name => 'test-stack'). 98 | and_return status_mock 99 | status_mock.stub(:wait_for_stable).and_return false 100 | stack_updater = SimpleDeploy::StackUpdater.new :name => 'test-stack', 101 | :template_body => @template_body, 102 | :entry => entry_mock 103 | 104 | lambda {stack_updater.update_stack( [ { 'param1' => 'new-value' } ] ) }. 105 | should raise_error 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/stack/status_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Status do 4 | include_context 'stubbed config' 5 | include_context 'double stubbed logger' 6 | 7 | before do 8 | @stack_reader_mock = mock 'stack reader mock' 9 | SimpleDeploy::StackReader.should_receive(:new). 10 | and_return @stack_reader_mock 11 | @status = SimpleDeploy::Status.new :name => 'test-stack' 12 | end 13 | 14 | after do 15 | SimpleDeploy.release_config 16 | end 17 | 18 | it "should return true if the stack is in complete state" do 19 | @stack_reader_mock.should_receive(:status). 20 | and_return 'UPDATE_COMPLETE' 21 | @status.complete?.should == true 22 | end 23 | 24 | it "should return false if the stack is not in complete state" do 25 | @stack_reader_mock.should_receive(:status). 26 | and_return 'UPDATE_IN_PROGRESS' 27 | @status.complete?.should == false 28 | end 29 | 30 | it "should return true if the stack is in a failed state" do 31 | @stack_reader_mock.should_receive(:status). 32 | and_return 'DELETE_FAILED' 33 | @status.failed?.should == true 34 | end 35 | 36 | it "should return false if the stack is not in a failed state" do 37 | @stack_reader_mock.should_receive(:status). 38 | and_return 'UPDATE_IN_PROGRESS' 39 | @status.failed?.should == false 40 | end 41 | 42 | it "should return true if the stack is in an in_progress state" do 43 | @stack_reader_mock.should_receive(:status).exactly(2).times. 44 | and_return 'UPDATE_IN_PROGRESS' 45 | @status.in_progress?.should == true 46 | end 47 | 48 | it "should return false if the stack is not in an in_progress state" do 49 | @stack_reader_mock.should_receive(:status).exactly(2).times. 50 | and_return 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS' 51 | @status.in_progress?.should == false 52 | end 53 | 54 | it "should return true if the stack is cleaning up" do 55 | @stack_reader_mock.should_receive(:status). 56 | and_return 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS' 57 | @status.cleanup_in_progress?.should == true 58 | end 59 | 60 | it "should return false if the stack is not in a cleaning up state" do 61 | @stack_reader_mock.should_receive(:status). 62 | and_return 'UPDATE_IN_PROGRESS' 63 | @status.cleanup_in_progress?.should == false 64 | end 65 | 66 | it "should return true if the stack is in a complete state" do 67 | @stack_reader_mock.should_receive(:status).exactly(2).times. 68 | and_return 'UPDATE_COMPLETE' 69 | @status.stable?.should == true 70 | end 71 | 72 | it "should return true if the stack is in a failed state" do 73 | @stack_reader_mock.should_receive(:status).exactly(3).times. 74 | and_return 'UPDATE_FAILED' 75 | @status.stable?.should == true 76 | end 77 | 78 | it "should return true if the stack is in an in progress state" do 79 | @stack_reader_mock.should_receive(:status).exactly(2).times. 80 | and_return 'IN_PROGRESS' 81 | @status.stable?.should == false 82 | end 83 | 84 | it "should return false if the stack creation failed" do 85 | @stack_reader_mock.should_receive(:status).exactly(3).times. 86 | and_return 'CREATE_FAILED' 87 | @status.stable?.should == false 88 | end 89 | 90 | it "should return true when the stack is in a stable state" do 91 | @stack_reader_mock.should_receive(:status).exactly(4).times. 92 | and_return 'CREATE_COMPLETE' 93 | @status.wait_for_stable.should == true 94 | end 95 | 96 | it "should sleep 2 times and return false when the stack is in an unstable state" do 97 | Kernel.stub!(:sleep) 98 | Kernel.should_receive(:sleep).with(1) 99 | Kernel.should_receive(:sleep).with(4) 100 | 101 | @stack_reader_mock.should_receive(:status).exactly(11).times. 102 | and_return 'CREATE_FAILED' 103 | @status.wait_for_stable(2).should == false 104 | end 105 | 106 | end 107 | -------------------------------------------------------------------------------- /spec/template_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleDeploy::Template do 4 | before do 5 | @contents = { 6 | "Parameters" => { 7 | "Test1" => { 8 | "Type" => "String", 9 | "Description" => "Test Param #1" 10 | }, 11 | "Test2" => { 12 | "Type" => "String", 13 | "Description" => "Test Param #2" 14 | } 15 | } 16 | }.to_json 17 | IO.should_receive(:read).with('/tmp/file').and_return @contents 18 | @template = SimpleDeploy::Template.new :file => '/tmp/file' 19 | end 20 | 21 | it "should return the parameters for a given template" do 22 | @template.parameters.should == ["Test1", "Test2"] 23 | end 24 | end 25 | --------------------------------------------------------------------------------