├── .gitignore ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE.txt ├── Makefile ├── README.md ├── Rakefile ├── bin └── gantree ├── circle.yml ├── gantree.gemspec ├── lib ├── gantree.rb └── gantree │ ├── app.rb │ ├── base.rb │ ├── cfn │ ├── beanstalk.rb │ ├── master.rb │ └── resources.rb │ ├── cli.rb │ ├── cli │ └── help.rb │ ├── config.rb │ ├── create.rb │ ├── delete.rb │ ├── deploy.rb │ ├── deploy_applications.rb │ ├── deploy_version.rb │ ├── docker.rb │ ├── init.rb │ ├── notification.rb │ ├── release_notes.rb │ ├── update.rb │ ├── version.rb │ └── wiki.rb ├── packaging ├── bundler-config └── wrapper.sh └── spec ├── fixtures └── project │ └── Dockerrun.aws.json ├── lib └── gantree │ ├── 1_release_notes_spec.rb │ ├── cli_spec.rb │ ├── deploy_spec.rb │ ├── deploy_version_spec.rb │ ├── docker_spec.rb │ ├── init_spec.rb │ └── wiki_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.gem 4 | *.rbc 5 | *.tar.gz 6 | .idea 7 | .bundle 8 | .config 9 | .yardoc 10 | InstalledFiles 11 | _yardoc 12 | coverage 13 | doc/ 14 | gantree-* 15 | lib/bundler/man 16 | packaging/vendor 17 | packaging/*.tar.gz 18 | pkg 19 | rdoc 20 | spec/reports 21 | test/tmp 22 | test/version_tmp 23 | tmp 24 | cfn/* 25 | tmp.txt 26 | 27 | spec/fixtures/project/*.zip 28 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.1.5 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | rvm: 4 | - 2.1.3 5 | before_script: 6 | - echo "{"https://index.docker.io/v1/":{"auth":"$DOCKER_TOKEN","email":"$DOCKER_EMAIL"}" > $HOME/.dockercfg 7 | - wget -P /$HOME/ http://bleacher-report.d.pr/1jaEC.gantreecfg 8 | env: 9 | global: 10 | - secure: OoLMnBOcoXTPOimdyh4ju8yW2YpLBXEOdljN1gVQzjIIv24cQ0BxWAAktjExntkYAjnRpxzTKK5ZkWadXm8NK7TtzFU80padk+WYVzuE1Xngxw/OhS1Z6NYL8+D3ortSANqoOB6r/Z13Sb5XClYDWlj5/GGzf4AerY5xgh2nJQc= 11 | - secure: MCPnXXEMJkamb3HuYS0H+YeMDKORYAekCfB8YMrtUeMR3QTSe2yQ6eMaNC/I2D5g8cNw60mRagaMyDMadZ4aVQ4sUK1YOHexs/vPJrVrIJU8ZGFsueQyboJun1NZLGvKAIAok7yAEEfwftCd+svYrtrTtV88fk9deOnBLs4o3gM= 12 | - secure: QZ00MAXO+Lblk/6Q0Bi+YQQLJtJYVHqMattxjIi72q5aOPQOSMT8UjDvOhDF2D/Le0wGBqVQxpxbpP/5cSGCLxtAfBijTyIirnkqV2td4hiXNfyQn3a+KnOifquJ8Vmo1tp/vvKGN16PIrLQCi5ARlcTj8QxfOFIihsrpelg9Ik= 13 | - secure: hs4c8HbrCmZzS1o1FFwymGKVgHDXahuh5h3Kp7mweHiGBV/QJsUy6sJq5oJMCZLPw6cqZ03NcpNaj+zwrt9pKkPnawyInLus6qrdv/OshMRRzFtXRT5HKt1u21KE3Qotg3+Cw8mPHCKHxi+Q2g57SyhM9ETZ74u83iqtBBLPXL8= 14 | - secure: DRrSBPqc17MxNAU7H57dDNp25c0NzsPZEk3LwGtkNTSBQaJVqDM52BESRgcjUMpW9yO4qg3KEVxyip/fYB2wjMRO8tXIgb86HJktOHv3flKE4OSKni+lfzTW02Pm02bRJJ4TbDvwk1NGrPar5mHcSpHQiCibmjlcv9gDXTd9ZRs= 15 | - secure: ovEq5bbK10ubB7PBiw+hN63L82Z0Azi8rGih5pnSI9VdHIViECKUM6FPciVFADA5xl+C4H/9oVxLaeDKqAsYvm1BsqBSnw4i8/0gmOa5twK4GpBPo4xkCMr9T5hA0xDGgN39AAEijJZVVmi+ecUpojHxVoxtEbjMK77Ud44mSMc= 16 | - secure: hpE4SKYumT1GAo5Q56DZ7q0DK7CT4rKp0MpnZLZnO7ob0TPy6UCQZViOaIRuLQ7aj5wzWToHdOPkzBIyE5H8DfV84lvsBYktFac5TLoiU+nxl/aSzO11tfIY0gneKEyjSjWIS0zgNXE7F7zTC6R46dPGpq8nnW7dEyZwDKmy8jw= 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in gantree.gemspec 4 | gem "codeclimate-test-reporter", group: :test, require: nil 5 | gem "aws-sdk-v1", "~>1.55.0" 6 | gem "guard", group: :test 7 | gem "guard-bundler", group: :test 8 | gem "rspec", group: :test 9 | gem "rake" 10 | gem "guard-rspec", require: false 11 | gem "webmock" 12 | gem "cloudformation-ruby-dsl", "0.4.6" 13 | gem "archive-zip", "~>0.7.0" 14 | gem "json" 15 | gem "slackr", "0.0.6" 16 | gem "highline" 17 | gem "pry" 18 | gem "vcr" 19 | gem "fakeweb" 20 | gem "colorize" 21 | gem "librato-metrics" 22 | gem "aws-sdk", "~>2.0.29" 23 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.8) 5 | aggregate (0.2.2) 6 | archive-zip (0.7.0) 7 | io-like (~> 0.3.0) 8 | aws-sdk (2.0.34) 9 | aws-sdk-resources (= 2.0.34) 10 | aws-sdk-core (2.0.34) 11 | builder (~> 3.0) 12 | jmespath (~> 1.0) 13 | multi_json (~> 1.0) 14 | aws-sdk-resources (2.0.34) 15 | aws-sdk-core (= 2.0.34) 16 | aws-sdk-v1 (1.55.0) 17 | json (~> 1.4) 18 | nokogiri (>= 1.4.4) 19 | builder (3.2.2) 20 | celluloid (0.16.0) 21 | timers (~> 4.0.0) 22 | cloudformation-ruby-dsl (0.4.6) 23 | detabulator 24 | codeclimate-test-reporter (0.4.7) 25 | simplecov (>= 0.7.1, < 1.0.0) 26 | coderay (1.1.0) 27 | colorize (0.7.5) 28 | crack (0.4.2) 29 | safe_yaml (~> 1.0.0) 30 | detabulator (0.1.0) 31 | diff-lcs (1.2.5) 32 | docile (1.1.5) 33 | fakeweb (1.3.0) 34 | faraday (0.9.1) 35 | multipart-post (>= 1.2, < 3) 36 | ffi (1.9.8) 37 | formatador (0.2.5) 38 | guard (2.12.5) 39 | formatador (>= 0.2.4) 40 | listen (~> 2.7) 41 | lumberjack (~> 1.0) 42 | nenv (~> 0.1) 43 | notiffany (~> 0.0) 44 | pry (>= 0.9.12) 45 | shellany (~> 0.0) 46 | thor (>= 0.18.1) 47 | guard-bundler (2.1.0) 48 | bundler (~> 1.0) 49 | guard (~> 2.2) 50 | guard-compat (~> 1.1) 51 | guard-compat (1.2.1) 52 | guard-rspec (4.5.0) 53 | guard (~> 2.1) 54 | guard-compat (~> 1.1) 55 | rspec (>= 2.99.0, < 4.0) 56 | highline (1.7.1) 57 | hitimes (1.2.2) 58 | io-like (0.3.0) 59 | jmespath (1.0.2) 60 | multi_json (~> 1.0) 61 | json (1.8.2) 62 | librato-metrics (1.5.0) 63 | aggregate (~> 0.2.2) 64 | faraday (~> 0.7) 65 | multi_json 66 | listen (2.10.0) 67 | celluloid (~> 0.16.0) 68 | rb-fsevent (>= 0.9.3) 69 | rb-inotify (>= 0.9) 70 | lumberjack (1.0.9) 71 | method_source (0.8.2) 72 | mini_portile (0.6.2) 73 | multi_json (1.11.0) 74 | multipart-post (2.0.0) 75 | nenv (0.2.0) 76 | nokogiri (1.6.6.2) 77 | mini_portile (~> 0.6.0) 78 | notiffany (0.0.6) 79 | nenv (~> 0.1) 80 | shellany (~> 0.0) 81 | pry (0.10.1) 82 | coderay (~> 1.1.0) 83 | method_source (~> 0.8.1) 84 | slop (~> 3.4) 85 | rake (10.4.2) 86 | rb-fsevent (0.9.4) 87 | rb-inotify (0.9.5) 88 | ffi (>= 0.5.0) 89 | rspec (3.2.0) 90 | rspec-core (~> 3.2.0) 91 | rspec-expectations (~> 3.2.0) 92 | rspec-mocks (~> 3.2.0) 93 | rspec-core (3.2.2) 94 | rspec-support (~> 3.2.0) 95 | rspec-expectations (3.2.0) 96 | diff-lcs (>= 1.2.0, < 2.0) 97 | rspec-support (~> 3.2.0) 98 | rspec-mocks (3.2.1) 99 | diff-lcs (>= 1.2.0, < 2.0) 100 | rspec-support (~> 3.2.0) 101 | rspec-support (3.2.2) 102 | safe_yaml (1.0.4) 103 | shellany (0.0.1) 104 | simplecov (0.9.2) 105 | docile (~> 1.1.0) 106 | multi_json (~> 1.0) 107 | simplecov-html (~> 0.9.0) 108 | simplecov-html (0.9.0) 109 | slackr (0.0.6) 110 | multipart-post (~> 2.0.0) 111 | slop (3.6.0) 112 | thor (0.19.1) 113 | timers (4.0.1) 114 | hitimes 115 | vcr (2.9.3) 116 | webmock (1.21.0) 117 | addressable (>= 2.3.6) 118 | crack (>= 0.3.2) 119 | 120 | PLATFORMS 121 | ruby 122 | 123 | DEPENDENCIES 124 | archive-zip (~> 0.7.0) 125 | aws-sdk (~> 2.0.29) 126 | aws-sdk-v1 (~> 1.55.0) 127 | cloudformation-ruby-dsl (= 0.4.6) 128 | codeclimate-test-reporter 129 | colorize 130 | fakeweb 131 | guard 132 | guard-bundler 133 | guard-rspec 134 | highline 135 | json 136 | librato-metrics 137 | pry 138 | rake 139 | rspec 140 | slackr (= 0.0.6) 141 | vcr 142 | webmock 143 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :rspec, cmd: 'bundle exec rspec' do 2 | watch(%r{^spec/.+_spec\.rb$}) 3 | watch(%r{^lib/(.+)\.rb$}) { "spec/cli_spec.rb" } 4 | watch(%r{^lib/gantree/(.+)\.rb$}) { "spec/cli_spec.rb" } 5 | watch('spec/spec_helper.rb') { "spec/gantree_spec.rb" } 6 | watch(%r{^lib/gantree/(.+)\.rb$}) { |m| "spec/lib/gantree/#{m[1]}_spec.rb" } 7 | end 8 | 9 | guard 'bundler' do 10 | watch('Gemfile') 11 | watch(/^.+\.gemspec/) 12 | end -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Tung Nguyen 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := 0.6.17 2 | 3 | # Change this variable to your organisation s3 bucket 4 | S3_BUCKET := s3://br-jenkins/gantree 5 | 6 | all: build 7 | 8 | version: 9 | @echo "${VERSION}" 10 | 11 | build: 12 | gem build gantree.gemspec 13 | 14 | install: 15 | gem install --local gantree-${VERSION}.gem 16 | 17 | linux: 18 | #docker run -ti --rm -v `pwd`:/workspace -w /workspace ruby:2.1.5 /bin/bash -c "bundle install && bundle exec rake package:linux:x86_64" 19 | @echo "=> push latest version linux binary to : ${S3_BUCKET}" 20 | aws s3 cp ./gantree-${VERSION}-linux-x86_64.tar.gz ${S3_BUCKET}/ --sse AES256 --acl public-read 21 | 22 | osx: 23 | docker run -ti --rm -v `pwd`:/workspace -w /workspace ruby:2.1.5 /bin/bash -c "bundle install && bundle exec rake package:osx" 24 | 25 | clean: 26 | rm -rf gantree-${VERSION}-*.tar.gz 27 | rm -rf gantree-${VERSION}.gem 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gantree 2 | 3 | [![Join the chat at https://gitter.im/feelobot/gantree](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/feelobot/gantree?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | [![Circle CI](https://circleci.com/gh/feelobot/gantree/tree/master.svg?style=svg)](https://circleci.com/gh/feelobot/gantree/tree/master) 6 | [![Test Coverage](https://codeclimate.com/github/feelobot/gantree/badges/coverage.svg)](https://codeclimate.com/github/feelobot/gantree) 7 | [![Code Climate](https://codeclimate.com/github/feelobot/gantree/badges/gpa.svg)](https://codeclimate.com/github/feelobot/gantree) 8 | [![Gem Version](https://badge.fury.io/rb/gantree.svg)](http://badge.fury.io/rb/gantree) 9 | ## Why Gantree? 10 | 11 | The name is derived from the word gantry which is a large crane used in ports to pick up shipping containers and load them on a ship. Gantry was already taken so I spelled it "tree" because the primary use is for elastic beanstalk and I guess a beanstalk is a form of tree? 12 | 13 | ## Description 14 | 15 | This tool is intended to help you setup a Dockerrun.aws.json which allows you to deploy a prebuilt image of your application to Elastic Beanstalk. This also allows you to do versioned deploys to your Elastic Beanstalk application and create an archive of every versioned Dockerrun.aws.json in amazons s3 bucket service. 16 | 17 | ## Installation 18 | 19 | ### Prerequisites 20 | You need to have your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables set in order to use the tool as well as the proper aws permissions for Elastic Beanstalk, and S3 access. 21 | 22 | Note : For gantree versions >= 0.6.14, configuration of AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables is not necessary if running on an instance with IAM Roles enabled. 23 | 24 | To check if your EC2 has iam role or not, run below command. 25 | 26 | ``` 27 | curl http://169.254.169.254/latest/meta-data/iam/info/ 28 | ``` 29 | 30 | *Install docker for MAC OSX* 31 | https://docs.docker.com/installation/mac/ 32 | 33 | Generate your login credentials token: 34 | ``` 35 | docker login 36 | ``` 37 | ### Setup 38 | ``` 39 | gem install gantree 40 | ``` 41 | 42 | ### Initialize a new Repo 43 | What this does is create a new Dockerrun.aws.json inside your repository and uploads your docker login credentials to s3 (for private repo access) so you can do deploys. We need the -u to specify a username to rename your .dockercfg and reference it in the Dockerrun.aws.json 44 | 45 | ``` 46 | # the username here is your docker.hub login 47 | gantree init -u frodriguez -p 3000 bleacher/cauldron:master 48 | # this will upload your docker config files to a bucket called "frodrigeuz-docker-cgfs" 49 | ``` 50 | If you don't have a docker.hub account, you can still use gantree without the `-u` flag, but you will have to explicitly specify the bucket for S3 storage since the default S3 bucket name is generated from the docker.hub login. 51 | 52 | ##### Specify the bucket for to S3 store docker configuration 53 | ``` 54 | # Since S3 bucket names are globally namespaced, the default bucket may be taken and unavailable 55 | # Gantree gives you the option to specify an S3 bucket name of your choice 56 | gantree init -u frodgriguez -p 3000 -b hopefully_this_bucket_name_is_available bleacher/cauldron:master 57 | ``` 58 | 59 | ### Deploy 60 | 61 | This command renames your Dockerrun.aws.json temporarily to NAME_OF_ENV-GITHUB_HASH-Dockerrun.aws.json, uploads it to a NAME_OF_APP-versions bucket, creates a new elastic beanstalk version, and asks the specified elastic beanstalk environment to update to that new version. 62 | 63 | ``` 64 | gantree deploy cauldron-stag-s1 65 | ``` 66 | By default this will check for the environment cauldron-stag-s1 and deploy to the app stag-cauldron-app. You can also specify an environment name directly using -e. 67 | 68 | ``` 69 | gantree deploy -e cauldron-stag-s1 stag-cauldron-app-s1 70 | ``` 71 | 72 | You can also specify a new image tag to use for the deploy 73 | 74 | ``` 75 | gantree deploy -t latest stag-cauldon-app-s1 76 | ``` 77 | 78 | ### Ship 79 | 80 | This command does three things: 81 | 82 | 1) Builds the image (with an auto generated tag (origin-branch-hash) or custom tag using the -t flag 83 | 2) Uploads the image based on the --image-path flag or gantree.cfg (ie. hub.docker, quay.io) 84 | 3) Deploys the image to elastic beanstalk based on command line arguments and cfn files. 85 | 86 | ``` 87 | # assuming you already ran 'gantree init' and generated the Dockerrun.aws.json file 88 | gantree ship linguist-stag-s2 -t branch-with-features-and-bug-fixes 89 | ``` 90 | 91 | ##### Autodetecting environment roles 92 | 93 | Let's say your linguist app consists of both an app and a worker. The Gantree ship command has an autodetect-app-role flag which will automatically detect the environment's role (app, worker, listener, etc.) by matching the environment name's third dash-delimited string and inject that as a `$ROLE` variable inside the Docker containers. 94 | 95 | For example, suppose this is the cfn.json file: 96 | 97 | ```json 98 | "Properties":{ 99 | "ApplicationName": "linguist-stag-s2", 100 | "EnvironmentName": "stag-linguist-app-s2", 101 | ... 102 | } 103 | 104 | "Properties":{ 105 | "ApplicationName": "linguist-stag-s2", 106 | "EnvironmentName": "stag-linguist-worker-s2", 107 | ... 108 | } 109 | ``` 110 | 111 | You can call gantree ship with the autodetect flag 112 | 113 | ``` 114 | gantree ship linguist-stag-s2 --autodetect-app-role=true -t branch-with-features-and-bug-fixes 115 | ``` 116 | 117 | Now every environment's Docker container will have the `$ROLE` variable: 118 | 119 | ``` 120 | # Inside stag-linguist-app-s2 Docker container 121 | root@adfd4543$ echo $ROLE 122 | app 123 | 124 | # Inside stag-linguist-worker-s2 Docker container 125 | root@afklji231$ echo $ROLE 126 | worker 127 | ``` 128 | 129 | As of Gantree version 0.4.9, the autodetect flag default is always on but if your env naming convention doesnt follow the stag-linguist-app-s2 convention, you should turn this off to avoid setting the wrong role variable in the Docker container. 130 | 131 | ## Update and Create 132 | As mentioned earlier, Gantree leverages the power of aws cloud formation. The gantree commands `update` and `create` are cloud formation specific gantree commands. 133 | 134 | ### Update 135 | We use gantree's `update` command to change an application name, environment name, or other cloud formation changes. 136 | ``` 137 | # after making changes in your /cfn/*cfn.json files, you want to update the stack 138 | gantree update linguist-stag-s1 139 | ``` 140 | 141 | ``` 142 | # You can use the -r flag to easily add a new role to the cnf template 143 | # Note the dry-run flag is used so we actually don't update the stack, but just changing the cfn files locally 144 | gantree update linguist-prod-s1 -r worker --dry-run 145 | ``` 146 | 147 | ### Create 148 | The gantree `create` command does three things: 149 | 150 | 1) Creates cfn templates 151 | 2) Sends them to s3 152 | 3) Creates the stack 153 | 154 | ``` 155 | gantree create linguist-prod-s1 156 | 157 | # you can use the --local flag to skip the first step 158 | gantree create linguist-prod-s1 --local 159 | ``` 160 | 161 | We use gantree's `create` command to create a new cfn template from an existing cfn template 162 | 163 | ``` 164 | # we already have linguist-prod-s1, and we want to create linguist-prod-s2 165 | gantree create linguist-prod-s2 --dupe linguist-prod-s1 166 | ``` 167 | 168 | ### .gantreecfg 169 | Allow defaults for commands to be set in a json file 170 | ```json 171 | { 172 | "ebextensions" : "git@github.com:br/.ebextensions.git", 173 | "default_instance_size" : "m3.medium" 174 | } 175 | ``` 176 | 177 | #### .ebextension Support 178 | Elastic Beanstalk cli allows you to create a .ebextension folder that you can package with your deploy to control the host/environment of your application. Deploying only a docker container image referenced in Dockerrun.aws.json has the unfortunate side effect of losing this extreamly powerful feature. To allow this feature to be included in gantree and make it even better you can select either to package a local .ebextension folder with your deploy, package a remote .ebextension folder hosted in github (with branch support) or even create a .gantreecfg file to make either of these type of deploys a default. 179 | 180 | ``` 181 | gantree deploy -x "git:br/ebextensions:master" stag-cauldron-app-s1 182 | ``` 183 | 184 | By default your application will be created on a t1.micro unless you specify otherwise: 185 | ``` 186 | gantree ceate your_app_name -i m3.medium 187 | ``` 188 | 189 | #### What if you need a database? Here enters the beauty of RDS 190 | 191 | PostgreSQL: ```gantree create your_app_name --rds pg``` 192 | 193 | Mysql: ```gantree create your_app_name --rds msql``` 194 | 195 | ## Traveling Ruby : 196 | 197 | You can now compile this into a tarball and distribute. 198 | 199 | #### To generate tarball & push to s3.ß 200 | ``` 201 | # Change below variable in Makefile before you run below command 202 | S3_BUCKET := s3:// 203 | make clean && make linux 204 | ``` 205 | #### To use gantree binary 206 | ``` 207 | aws s3 cp s3:///gantree-0.6.15-linux-x86_64.tar.gz . 208 | tar -xf gantree-0.6.15-linux-x86_64.tar.gz -C /opt 209 | ln -s /opt/gantree-0.6.15-linux-x86_64/gantree /usr/bin/gantree 210 | ``` 211 | 212 | ## TODO: 213 | 214 | #### What if you want a cdn behind each of your generated applications 215 | 216 | Fastly: ```gantree create your_app_name --cdn fastly``` 217 | 218 | CloudFront: ```gantree create yoour_app_name --cdn cloudfront``` 219 | 220 | #### Redis & Memcached 221 | Elasticache ```gantree create your_app_name --cache redis``` or ```gantree create your_app_name --cache memcache``` 222 | 223 | #### Autogenerated Release Notes 224 | 225 | I would like have built in integration with opbeat configurable thorugh the .gantreecfg located in the applications repository. 226 | 227 | 228 | ***Notes:*** 229 | 230 | Also if the cloud formation template that is generated doesn't match your needs (which it might now) you can edit the .rb files in the repo's cfn folder, build your own gem and use it how you like. 231 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'bundler/setup' 3 | require 'rspec/core/rake_task' 4 | require 'gantree/version' 5 | 6 | PACKAGE_NAME = "gantree" 7 | VERSION = Gantree::VERSION 8 | TRAVELING_RUBY_VERSION = "20150210-2.1.5" 9 | 10 | task :default => :spec 11 | 12 | RSpec::Core::RakeTask.new 13 | 14 | namespace :spec do 15 | desc "Run acceptance specs, forces AWS calls by cleaning vcr fixtures first, ~/.br/aws.yml needs to be set up" 16 | task :acceptance => %w[clean:vcr] do 17 | ENV['LIVE'] = "1" 18 | Rake::Task["spec"].invoke 19 | end 20 | end 21 | 22 | task :clean => %w[clean:vcr] 23 | namespace :clean do 24 | desc "clean vcr_cassettes fixtures" 25 | task :vcr do 26 | FileUtils.rm_rf('spec/fixtures/vcr_cassettes') 27 | end 28 | end 29 | 30 | 31 | namespace :package do 32 | namespace :linux do 33 | desc "Package your app for Linux x86" 34 | task :x86 => [:bundle_install, "packaging/traveling-ruby-#{TRAVELING_RUBY_VERSION}-linux-x86.tar.gz"] do 35 | create_package("linux-x86") 36 | end 37 | 38 | desc "Package your app for Linux x86_64" 39 | task :x86_64 => [:bundle_install, "packaging/traveling-ruby-#{TRAVELING_RUBY_VERSION}-linux-x86_64.tar.gz"] do 40 | create_package("linux-x86_64") 41 | end 42 | end 43 | 44 | desc "Package your app for OS X" 45 | task :osx => [:bundle_install, "packaging/traveling-ruby-#{TRAVELING_RUBY_VERSION}-osx.tar.gz"] do 46 | create_package("osx") 47 | end 48 | 49 | desc "Install gems to local directory" 50 | task :bundle_install do 51 | if RUBY_VERSION !~ /^2\.1\./ 52 | abort "You can only 'bundle install' using Ruby 2.1, because that's what Traveling Ruby uses." 53 | end 54 | sh "rm -rf packaging/tmp" 55 | sh "mkdir packaging/tmp" 56 | sh "cp Gemfile Gemfile.lock packaging/tmp/" 57 | Bundler.with_clean_env do 58 | sh "cd packaging/tmp && env BUNDLE_IGNORE_CONFIG=1 bundle install --path ../vendor --without development" 59 | end 60 | sh "rm -rf packaging/tmp" 61 | sh "rm -f packaging/vendor/*/*/cache/*" 62 | end 63 | end 64 | 65 | file "packaging/traveling-ruby-#{TRAVELING_RUBY_VERSION}-linux-x86.tar.gz" do 66 | download_runtime("linux-x86") 67 | end 68 | 69 | file "packaging/traveling-ruby-#{TRAVELING_RUBY_VERSION}-linux-x86_64.tar.gz" do 70 | download_runtime("linux-x86_64") 71 | end 72 | 73 | file "packaging/traveling-ruby-#{TRAVELING_RUBY_VERSION}-osx.tar.gz" do 74 | download_runtime("osx") 75 | end 76 | 77 | file "packaging/traveling-ruby-#{TRAVELING_RUBY_VERSION}-osx-nokogiri-1.6.5.tar.gz" do 78 | download_native_extension("osx", "nokogiri-1.6.5") 79 | end 80 | 81 | file "packaging/traveling-ruby-#{TRAVELING_RUBY_VERSION}-linux-x86-nokogiri-1.6.5.tar.gz" do 82 | download_native_extension("linux-x86", "nokogiri-1.6.5") 83 | end 84 | 85 | file "packaging/traveling-ruby-#{TRAVELING_RUBY_VERSION}-linux-x86_64-nokogiri-1.6.5.tar.gz" do 86 | download_native_extension("linux-x86_64", "nokogiri-1.6.5") 87 | end 88 | 89 | def download_runtime(target) 90 | sh "cd packaging && curl -L -O --fail " + 91 | "https://d6r77u77i8pq3.cloudfront.net/releases/traveling-ruby-#{TRAVELING_RUBY_VERSION}-#{target}.tar.gz" 92 | end 93 | 94 | def download_native_extension(target, gem_name_and_version) 95 | sh "curl -L --fail -o packaging/traveling-ruby-#{TRAVELING_RUBY_VERSION}-#{target}-#{gem_name_and_version}.tar.gz " + 96 | "https://d6r77u77i8pq3.cloudfront.net/releases/traveling-ruby-gems-#{TRAVELING_RUBY_VERSION}-#{target}/#{gem_name_and_version}.tar.gz" 97 | end 98 | 99 | def create_package(target) 100 | package_dir = "#{PACKAGE_NAME}-#{VERSION}-#{target}" 101 | sh "rm -rf #{package_dir}" 102 | sh "mkdir #{package_dir}" 103 | sh "mkdir -p #{package_dir}/app/bin" 104 | sh "mkdir -p #{package_dir}/lib/app" 105 | sh "mkdir #{package_dir}/lib/ruby" 106 | sh "mkdir #{package_dir}/bin" 107 | sh "tar -xzf packaging/traveling-ruby-#{TRAVELING_RUBY_VERSION}-#{target}.tar.gz -C #{package_dir}/lib/ruby" 108 | sh "cp packaging/wrapper.sh #{package_dir}/gantree" 109 | sh "chmod +x #{package_dir}/gantree" 110 | sh "cp -pR packaging/vendor #{package_dir}/lib/" 111 | sh "cp Gemfile Gemfile.lock #{package_dir}/lib/vendor/" 112 | sh "mkdir #{package_dir}/lib/vendor/.bundle" 113 | sh "cp packaging/bundler-config #{package_dir}/lib/vendor/.bundle/config" 114 | sh "cp bin/gantree #{package_dir}/app/bin/gantree" 115 | sh "cp -a lib #{package_dir}/app" 116 | if !ENV['DIR_ONLY'] 117 | sh "tar -czf #{package_dir}.tar.gz #{package_dir}" 118 | sh "rm -rf #{package_dir}" 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /bin/gantree: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | 5 | $:.unshift(File.expand_path('../../lib', __FILE__)) 6 | require 'gantree' 7 | require 'gantree/cli' 8 | 9 | Gantree::CLI.start(ARGV) -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | ruby: 3 | version: 2.1.3 4 | services: 5 | - docker 6 | dependencies: 7 | pre: 8 | - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS 9 | - wget -P $HOME/gantree/ http://bleacher-report.d.pr/1jaEC.gantreecfg 10 | - mv $HOME/gantree/1jaEC.gantreecfg $HOME/gantree/.gantreecfg 11 | test: 12 | override: 13 | - bundle exec rake 14 | -------------------------------------------------------------------------------- /gantree.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'gantree/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "gantree" 8 | spec.version = Gantree::VERSION 9 | spec.authors = ["Felix"] 10 | spec.email = ["felix.a.rod@gmail.com"] 11 | spec.description = "cli tool for automating docker deploys to elastic beanstalk" 12 | spec.summary = "This tool is intended to help you setup a Dockerrun.aws.json which allows you to deploy a prebuilt image of your application to Elastic Beanstalk. This also allows you to do versioned deploys to your Elastic Beanstalk application and create an archive of every versioned Dockerrun.aws.json in amazons s3 bucket service." 13 | spec.homepage = "https://github.com/feelobot/gantree" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "thor" 22 | spec.add_dependency "aws-sdk-v1", "~>1.55.0" 23 | spec.add_dependency "aws-sdk", "~>2.1.0" 24 | spec.add_dependency "hashie" 25 | spec.add_dependency "colorize" 26 | spec.add_dependency "rubyzip" 27 | spec.add_dependency "cloudformation-ruby-dsl","0.4.6" 28 | spec.add_dependency "archive-zip","~>0.7.0" 29 | spec.add_dependency "json" 30 | spec.add_dependency "slackr","0.0.6" 31 | spec.add_dependency "highline" 32 | spec.add_dependency "pry" 33 | spec.add_dependency "librato-metrics" 34 | 35 | spec.add_development_dependency "bundler", "~> 1.3" 36 | spec.add_development_dependency "rake" 37 | spec.add_development_dependency "guard" 38 | spec.add_development_dependency "guard-bundler" 39 | spec.add_development_dependency "guard-rspec" 40 | spec.add_development_dependency "vcr" 41 | end 42 | 43 | -------------------------------------------------------------------------------- /lib/gantree.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.expand_path("../", __FILE__)) 2 | require "gantree/base" 3 | require "gantree/config" 4 | require "gantree/version" 5 | require "gantree/deploy" 6 | require "gantree/deploy_applications" 7 | require "gantree/deploy_version" 8 | require "gantree/init" 9 | require "gantree/delete" 10 | require "gantree/update" 11 | require "gantree/create" 12 | require "gantree/app" 13 | require "gantree/docker" 14 | 15 | module Gantree 16 | autoload :CLI, 'gantree/cli' 17 | end 18 | 19 | -------------------------------------------------------------------------------- /lib/gantree/app.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' 2 | 3 | module Gantree 4 | class App < Base 5 | 6 | def initialize env, options 7 | check_credentials 8 | set_aws_keys 9 | @env = env 10 | end 11 | 12 | def restart 13 | eb.restart_app_server({ environment_name: "#{@env}" }) 14 | puts "App is now restarting".green 15 | end 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /lib/gantree/base.rb: -------------------------------------------------------------------------------- 1 | require "colorize" 2 | require 'librato/metrics' 3 | require 'bigdecimal' 4 | require 'bigdecimal/util' 5 | require 'net/http' 6 | 7 | module Gantree 8 | class Base 9 | def check_credentials 10 | timeout = false 11 | 12 | url = URI.parse("http://169.254.169.254/latest/meta-data/iam/info/") 13 | 14 | http = Net::HTTP.new(url.host, url.port) 15 | 16 | http.read_timeout = 3 17 | http.open_timeout = 3 18 | 19 | begin 20 | resp = http.start() { |http| http.get(url.path) }.code 21 | rescue Net::OpenTimeout 22 | timeout = true 23 | end 24 | 25 | if timeout == false && resp == '200' 26 | puts "Using IAM Role".green 27 | else 28 | raise "Please set your AWS Environment Variables" unless ENV['AWS_SECRET_ACCESS_KEY'] 29 | raise "Please set your AWS Environment Variables" unless ENV['AWS_ACCESS_KEY_ID'] 30 | end 31 | end 32 | 33 | def self.check_for_updates opts 34 | puts opts.inspect 35 | enabled = opts[:auto_updates] 36 | #return if $0.include? "bin/gantree" 37 | latest_version = `gem search gantree | grep gantree | awk '{ print $2 }' | tr -d '()'`.strip 38 | current_version = VERSION 39 | puts "Auto updates enabled".light_blue if enabled 40 | puts "Auto updates disabled".light_blue if !enabled 41 | puts "Updating from #{current_version} to #{latest_version}...".green 42 | if current_version == latest_version && enabled 43 | system("gem update gantree --force") 44 | else 45 | puts "gem already up to date".light_blue 46 | end 47 | update_configuration(opts[:auto_configure]) if opts[:auto_configure] 48 | Gantree::Config.merge_defaults(opts) 49 | end 50 | 51 | def self.update_configuration repo 52 | puts "Auto configuring from #{repo}".light_blue 53 | FileUtils.rm_rf("/tmp/gantreecfg") if File.directory?("/tmp/gantreecfg") 54 | system("git clone #{repo} /tmp/gantreecfg") 55 | FileUtils.cp("/tmp/gantreecfg/.gantreecfg","#{ENV['HOME']}/") 56 | puts "Auto Configured".green 57 | end 58 | 59 | def print_options 60 | @options.each do |param, value| 61 | puts "#{param}: #{value}" 62 | end 63 | end 64 | 65 | def set_aws_keys 66 | unless ENV['AWS_ACCESS_KEY_ID'] == nil && ENV['AWS_SECRET_ACCESS_KEY'] == nil 67 | AWS.config( 68 | :access_key_id => ENV['AWS_ACCESS_KEY_ID'], 69 | :secret_access_key => ENV['AWS_SECRET_ACCESS_KEY'] 70 | ) 71 | else 72 | AWS.config(:credential_provider => AWS::Core::CredentialProviders::EC2Provider.new) 73 | end 74 | end 75 | 76 | def s3 77 | @s3 ||= AWS::S3.new 78 | end 79 | 80 | def eb 81 | @eb ||= AWS::ElasticBeanstalk::Client.new 82 | end 83 | 84 | def cfm 85 | @cfm ||= AWS::CloudFormation.new 86 | end 87 | 88 | 89 | def tag 90 | origin = `git config --get remote.origin.url`.match("com(.*)\/")[1].gsub(":","").gsub("/","").strip 91 | branch = `git rev-parse --abbrev-ref HEAD`.strip 92 | hash = `git rev-parse --verify --short #{branch}`.strip 93 | "#{origin}-#{branch.gsub('-','')}-#{hash}".gsub("/", "").downcase 94 | rescue 95 | puts "ERROR: Using outside of a git repository".red 96 | end 97 | 98 | def create_default_env 99 | tags = @options[:stack_name].split("-") 100 | if tags.length == 3 101 | [tags[1],tags[0],"app",tags[2]].join('-') 102 | else 103 | raise "Please Set Envinronment Name with -e" 104 | end 105 | end 106 | 107 | def authenticate_librato 108 | if @options[:librato] 109 | Librato::Metrics.authenticate @options[:librato][:email], @options[:librato][:token] 110 | end 111 | end 112 | 113 | def env_type 114 | if @options[:env].include?("prod") 115 | "prod" 116 | elsif @options[:env].include?("stag") 117 | "stag" 118 | else 119 | "" 120 | end 121 | end 122 | 123 | def get_latest_docker_solution 124 | result = eb.list_available_solution_stacks 125 | solutions = result[:solution_stacks] 126 | docker_solutions = solutions.select { |s| s.include? "running Docker"} 127 | docker_solutions.first 128 | end 129 | 130 | 131 | def escape_characters_in_string(string) 132 | pattern = /(\'|\"|\.|\*|\/|\-|\\)/ 133 | string.gsub(pattern){|match|"\\" + match} # <-- Trying to take the currently found match and add a \ before it I have no idea how to do that). 134 | end 135 | 136 | def upload_templates 137 | check_template_bucket 138 | @templates.each do |template| 139 | filename = "cfn/#{@options[:stack_name]}-#{template}.cfn.json" 140 | key = File.basename(filename) 141 | s3.buckets["#{@options[:cfn_bucket]}/#{@options[:stack_name]}"].objects[key].write(:file => filename) 142 | end 143 | puts "templates uploaded" 144 | end 145 | 146 | def check_template_bucket 147 | puts "DEBUG: #{@options[:cfn_bucket]}" 148 | raise "Set Bucket to Upload Templates with --cfn-bucket" unless @options[:cfn_bucket] 149 | bucket_name = "#{@options[:cfn_bucket]}/#{@options[:stack_name]}" 150 | if s3.buckets[bucket_name].exists? 151 | puts "uploading cfn templates to #{@options[:cfn_bucket]}/#{@options[:stack_name]}" 152 | else 153 | puts "creating bucket #{@options[:cfn_bucket]}/#{@options[:stack_name]} to upload templates" 154 | s3.buckets.create(bucket_name) 155 | end 156 | end 157 | 158 | def error_msg msg 159 | puts msg.red 160 | exit 1 161 | end 162 | end 163 | end 164 | 165 | -------------------------------------------------------------------------------- /lib/gantree/cfn/beanstalk.rb: -------------------------------------------------------------------------------- 1 | class BeanstalkTemplate 2 | 3 | def initialize params 4 | @stack_name = params[:stack_name] 5 | @docker_version = params[:solution] 6 | @size = params[:instance_size] 7 | @rds = params[:rds] 8 | @env = params[:env] 9 | @domain = params[:domain] 10 | @requirements = params[:requirements] 11 | @rds_enabled = params[:rds?] 12 | @env_type = params[:env_type] 13 | end 14 | 15 | def create 16 | "#{@requirements} 17 | template do 18 | 19 | value :AWSTemplateFormatVersion => '2010-09-09' 20 | 21 | value :Description => '#{@stack_name} Service Parent Template (2014-08-15)' 22 | 23 | #{beanstalk_parmaters} 24 | 25 | resource 'Application', :Type => 'AWS::ElasticBeanstalk::Application', :Properties => { 26 | :Description => '#{@stack_name}', 27 | :ApplicationName => '#{@stack_name}', 28 | } 29 | 30 | #{configuration_template} 31 | 32 | #{resources} 33 | 34 | output 'URL', 35 | :Description => 'URL of the AWS Elastic Beanstalk Environment', 36 | :Value => join('', 'http://', get_att('EbEnvironment', 'EndpointURL')) 37 | 38 | end.exec! 39 | " 40 | end 41 | 42 | def beanstalk_parmaters 43 | "parameter 'KeyName', 44 | :Description => 'The Key Pair to launch instances with', 45 | :Type => 'String', 46 | :Default => 'default' 47 | 48 | parameter 'InstanceSecurityGroup', 49 | :Type => 'String' 50 | 51 | parameter 'InstanceType', 52 | :Description => 'EC2 Instance Type', 53 | :Type => 'String', 54 | :Default => '#{@size}' 55 | 56 | parameter 'ApplicationName', 57 | :Description => 'The name of the Elastic Beanstalk Application', 58 | :Type => 'String', 59 | :Default => '#{@stack_name}' 60 | 61 | parameter 'Environment', 62 | :Type => 'String', 63 | :Default => '#{@env_type}' 64 | 65 | parameter 'IamInstanceProfile', 66 | :Type => 'String', 67 | :Default => 'EbApp' 68 | 69 | #{"parameter 'RDSHostURLPass', :Type => 'String'" if @rds_enabled }" 70 | 71 | end 72 | 73 | def configuration_template 74 | "resource 'ConfigurationTemplate', :Type => 'AWS::ElasticBeanstalk::ConfigurationTemplate', :Properties => { 75 | :ApplicationName => ref('Application'), 76 | :SolutionStackName => '#{@docker_version}', 77 | :Description => 'Default Configuration Version #{@docker_version} - with SSH access', 78 | :OptionSettings => [ 79 | { 80 | :Namespace => 'aws:elasticbeanstalk:application:environment', 81 | :OptionName => 'AWS_REGION', 82 | :Value => aws_region, 83 | }, 84 | { 85 | :Namespace => 'aws:autoscaling:launchconfiguration', 86 | :OptionName => 'EC2KeyName', 87 | :Value => ref('KeyName'), 88 | }, 89 | { 90 | :Namespace => 'aws:autoscaling:launchconfiguration', 91 | :OptionName => 'IamInstanceProfile', 92 | :Value => ref('IamInstanceProfile'), 93 | }, 94 | { 95 | :Namespace => 'aws:autoscaling:launchconfiguration', 96 | :OptionName => 'InstanceType', 97 | :Value => ref('InstanceType'), 98 | }, 99 | { 100 | :Namespace => 'aws:autoscaling:launchconfiguration', 101 | :OptionName => 'SecurityGroups', 102 | :Value => join(',', join('-', '#{@env_type}', 'br'), ref('InstanceSecurityGroup')), 103 | }, 104 | { :Namespace => 'aws:autoscaling:updatepolicy:rollingupdate', :OptionName => 'RollingUpdateEnabled', :Value => 'true' }, 105 | { :Namespace => 'aws:autoscaling:updatepolicy:rollingupdate', :OptionName => 'MaxBatchSize', :Value => '1' }, 106 | { :Namespace => 'aws:autoscaling:updatepolicy:rollingupdate', :OptionName => 'MinInstancesInService', :Value => '2' }, 107 | { :Namespace => 'aws:elasticbeanstalk:hostmanager', :OptionName => 'LogPublicationControl', :Value => 'true' }, 108 | #{set_rds_parameters if @rds_enabled } 109 | ], 110 | }" 111 | end 112 | 113 | def resources 114 | "resource 'EbEnvironment', :Type => 'AWS::ElasticBeanstalk::Environment', :Properties => { 115 | :ApplicationName => '#{@stack_name}', 116 | :EnvironmentName => '#{@env}', 117 | :Description => 'Default Environment', 118 | :TemplateName => ref('ConfigurationTemplate'), 119 | :OptionSettings => [], 120 | } 121 | 122 | resource 'HostRecord', :Type => 'AWS::Route53::RecordSet', :Properties => { 123 | :Comment => 'DNS name for my stack', 124 | :HostedZoneName => '#{@domain}', 125 | :Name => join('.', '#{@stack_name}', '#{@domain}'), 126 | :ResourceRecords => [ get_att('EbEnvironment', 'EndpointURL') ], 127 | :TTL => '60', 128 | :Type => 'CNAME', 129 | }" 130 | end 131 | def set_rds_parameters 132 | "{ 133 | :Namespace => 'aws:elasticbeanstalk:application:environment', 134 | :OptionName => 'DB_HostURL', 135 | :Value => ref('RDSHostURLPass'), 136 | }," 137 | end 138 | end -------------------------------------------------------------------------------- /lib/gantree/cfn/master.rb: -------------------------------------------------------------------------------- 1 | class MasterTemplate 2 | 3 | def initialize params 4 | @stack_name = params[:stack_name] 5 | @rds = params[:rds] 6 | @rds_enabled = params[:rds?] 7 | @env = params[:env] 8 | @bucket = params[:cfn_bucket] 9 | @requirements = params[:requirements] 10 | end 11 | 12 | def create 13 | "#{@requirements} 14 | template do 15 | value :AWSTemplateFormatVersion => '2010-09-09' 16 | value :Description => '#{@stack_name} Master Template' 17 | 18 | parameter 'ResourcesTemplate', 19 | :Description => 'The key of the template for the resources required to run the app', 20 | :Type => 'String', 21 | :Default => '#{@stack_name}-resources.cfn.json' 22 | 23 | parameter 'AppTemplate', 24 | :Description => 'The key of the template for the EB app/env substack', 25 | :Type => 'String', 26 | :Default => '#{@stack_name}-beanstalk.cfn.json' 27 | 28 | parameter 'KeyName', 29 | :Type => 'String', 30 | :Default => 'default' 31 | 32 | parameter 'ApplicationName', 33 | :Type => 'String', 34 | :Default => '#{@stack_name}' 35 | 36 | parameter 'Environment', 37 | :Type => 'String', 38 | :Default => '#{@env_type}' 39 | 40 | parameter 'IamInstanceProfile', 41 | :Type => 'String', 42 | :Default => 'EbApp' 43 | 44 | resource 'AppResources', :Type => 'AWS::CloudFormation::Stack', :Properties => { 45 | :TemplateURL => join('/', 'http://s3.amazonaws.com', '#{@bucket}', '#{@stack_name}', ref('ResourcesTemplate')), 46 | :Parameters => { :ApplicationName => ref('ApplicationName') }, 47 | } 48 | 49 | resource 'App', :Type => 'AWS::CloudFormation::Stack', :Properties => { 50 | :TemplateURL => join('/', 'http://s3.amazonaws.com','#{@bucket}', '#{@stack_name}', ref('AppTemplate')), 51 | :Parameters => { 52 | :KeyName => ref('KeyName'), 53 | :InstanceSecurityGroup => get_att('AppResources', 'Outputs.InstanceSecurityGroup'), 54 | :ApplicationName => ref('ApplicationName'), 55 | :Environment => ref('Environment'), 56 | :IamInstanceProfile => ref('IamInstanceProfile'), 57 | #{":RDSHostURLPass => get_att('AppResources','Outputs.RDSHostURL')," if @rds_enabled} 58 | }, 59 | } 60 | 61 | output 'URL', 62 | :Description => 'URL of the AWS Elastic Beanstalk Environment', 63 | :Value => get_att('App', 'Outputs.URL') 64 | 65 | end.exec!" 66 | end 67 | end -------------------------------------------------------------------------------- /lib/gantree/cfn/resources.rb: -------------------------------------------------------------------------------- 1 | class ResourcesTemplate 2 | 3 | def initialize params 4 | @stack_name = params[:stack_name] 5 | @rds = params[:rds] 6 | @env = params[:env] 7 | @rds_enabled = params[:rds?] 8 | @requirements = params[:requirements] 9 | @env_type = params[:env_type] 10 | end 11 | 12 | def create 13 | "#{@requirements} 14 | template do 15 | 16 | value :AWSTemplateFormatVersion => '2010-09-09' 17 | 18 | value :Description => '#{@stack_name} Services Resources (2014-06-30)' 19 | 20 | parameter 'ApplicationName', 21 | :Type => 'String', 22 | :Default => '#{@stack_name}' 23 | 24 | resource 'InstanceSecurityGroup', :Type => 'AWS::EC2::SecurityGroup', :Properties => { 25 | :GroupDescription => join('', 'an EC2 instance security group created for #{@stack_name}') 26 | } 27 | 28 | output 'InstanceSecurityGroup', 29 | :Value => ref('InstanceSecurityGroup') 30 | 31 | #{rds if @rds_enabled} 32 | 33 | end.exec! 34 | " 35 | end 36 | 37 | def rds 38 | "resource 'sampleDB', :Type => 'AWS::RDS::DBInstance', :DeletionPolicy => 'Snapshot', :Properties => { 39 | :DBName => 'sampledb', 40 | :AllocatedStorage => '10', 41 | :DBInstanceClass => 'db.m3.large', 42 | :DBSecurityGroups => [ ref('DBSecurityGroup') ], 43 | :Engine => 'postgres', 44 | :EngineVersion => '9.3', 45 | :MasterUsername => 'masterUser', 46 | :MasterUserPassword => 'masterpassword', 47 | } 48 | 49 | resource 'DBSecurityGroup', :Type => 'AWS::RDS::DBSecurityGroup', :Properties => { 50 | :DBSecurityGroupIngress => [ 51 | { :EC2SecurityGroupName => ref('InstanceSecurityGroup') }, 52 | ], 53 | :GroupDescription => 'Allow Beanstalk Instances Access', 54 | } 55 | 56 | output 'RDSHostURL', 57 | :Value => get_att('sampleDB', 'Endpoint.Address') 58 | " 59 | end 60 | end -------------------------------------------------------------------------------- /lib/gantree/cli.rb: -------------------------------------------------------------------------------- 1 | require "pry" 2 | require 'thor' 3 | require 'aws-sdk-v1' 4 | require 'gantree/cli/help' 5 | 6 | module Gantree 7 | class CLI < Thor 8 | 9 | class_option :dry_run, :aliases => "-d", :desc => "dry run mode", :default => false, :type => :boolean 10 | 11 | desc "deploy APP", "deploy specified APP" 12 | long_desc Help.deploy 13 | option :tag, :aliases => "-t", :desc => "set docker tag to deploy", :default => Gantree::Base.new.tag 14 | option :ext, :aliases => "-x", :desc => "ebextensions folder/repo" 15 | option :ext_role, :desc => "role based extension repo (bleacher specific)" 16 | option :silent, :aliases => "-s", :desc => "mute notifications" 17 | option :image_path, :aliases => "-i", :desc => "docker hub image path ex. (bleacher/cms | quay.io/bleacherreport/cms)" 18 | option :autodetect_app_role, :desc => "use naming convention to determin role (true|false)", :type => :boolean, :default => true 19 | option :eb_bucket, :desc => "bucket to store elastic beanstalk versions" 20 | option :auth, :desc => "dockerhub authentation, example: bucket/key" 21 | option :release_notes_staging, :type => :boolean, :default => false, :desc => "force release notes generation for staging deploys" 22 | def deploy name 23 | opts = Gantree::Config.merge_defaults(options) 24 | opts = Gantree::Base.check_for_updates(opts) 25 | Gantree::Deploy.new(name,opts).run 26 | end 27 | 28 | desc "init IMAGE", "create a dockerrun for your IMAGE" 29 | long_desc Help.init 30 | method_option :user , :aliases => "-u", :desc => "user credentials for private dockerhub repo" 31 | method_option :port , :aliases => "-p", :desc => "port of running application" 32 | method_option :bucket , :aliases => "-b", :desc => "set bucket name, default is '-docker-cfgs'" 33 | def init image 34 | Gantree::Init.new(image, options).run 35 | end 36 | 37 | desc "create APP", "create a cfn stack" 38 | long_desc Help.create 39 | option :cfn_bucket, :desc => "s3 bucket to store cfn templates" 40 | option :domain, :desc => "route53 domain" 41 | option :env, :aliases => "-e", :desc => "(optional) environment name" 42 | option :instance_size, :aliases => "-i", :desc => "(optional) set instance size", :default => "m3.medium" 43 | option :rds, :aliases => "-r", :desc => "(optional) set database type [pg,mysql]" 44 | option :solution, :aliases => "-s", :desc => "change solution stack" 45 | option :dupe, :alias => "-d", :desc => "copy an existing template into a new template" 46 | option :local, :alias => "-l", :desc => "use a local cfn nested template" 47 | def create app 48 | Gantree::Create.new(app, Gantree::Config.merge_defaults(options)).run 49 | end 50 | 51 | desc "update APP", "update a cfn stack" 52 | long_desc Help.update 53 | option :cfn_bucket, :desc => "s3 bucket to store cfn templates" 54 | option :role, :aliases => "-r", :desc => "add an app role (worker|listner|scheduler)" 55 | option :solution, :aliases => "-s", :desc => "change solution stack" 56 | def update app 57 | Gantree::Update.new(app, Gantree::Config.merge_defaults(options)).run 58 | end 59 | 60 | desc "delete APP", "delete a cfn stack" 61 | option :force, :desc => "do not prompt", :default => false 62 | def delete app 63 | Gantree::Delete.new(app, Gantree::Config.merge_defaults(options)).run 64 | end 65 | 66 | desc "restart APP", "restart an eb app" 67 | def restart app 68 | Gantree::App.new(app, Gantree::Config.merge_defaults(options)).restart 69 | end 70 | 71 | desc "build", "build and tag a docker application" 72 | long_desc Help.build 73 | option :image_path, :aliases => "-i", :desc => "docker hub image path ex. (bleacher/cms | quay.io/bleacherreport/cms)" 74 | option :tag, :aliases => "-t", :desc => "set docker tag to build" 75 | def build 76 | docker = Gantree::Docker.new(Gantree::Config.merge_defaults(options)) 77 | docker.build 78 | end 79 | 80 | desc "push", "build and tag a docker application" 81 | long_desc Help.push 82 | option :hub, :aliases => "-h", :desc => "hub (docker|quay)" 83 | option :image_path, :aliases => "-i", :desc => "docker hub image path ex. (bleacher/cms | quay.io/bleacherreport/cms)" 84 | option :tag, :aliases => "-t", :desc => "set docker tag to push" 85 | def push 86 | Gantree::Docker.new(Gantree::Config.merge_defaults(options)).push 87 | end 88 | 89 | desc "tag", "tag a docker application" 90 | def tag 91 | puts Gantree::Base::new.tag 92 | end 93 | 94 | desc "ship", "build, push and deploy docker container to elastic beanstalk" 95 | long_desc Help.ship 96 | option :tag, :aliases => "-t", :desc => "set docker tag to deploy", :default => Gantree::Base.new.tag 97 | option :ext, :aliases => "-x", :desc => "ebextensions folder/repo" 98 | option :silent, :aliases => "-s", :desc => "mute notifications" 99 | option :autodetect_app_role, :desc => "use naming convention to determin role (true|flase)", :type => :boolean 100 | option :image_path, :aliases => "-i", :desc => "hub image path ex. (bleacher/cms | quay.io/bleacherreport/cms)" 101 | option :hush, :desc => "quite puts messages", :default => true 102 | option :eb_bucket, :desc => "bucket to store elastic beanstalk versions" 103 | option :auth, :desc => "dockerhub authentation, example: bucket/key" 104 | option :release_notes_staging, :type => :boolean, :default => false, :desc => "force release notes generation for staging deploys" 105 | def ship server 106 | opts = Gantree::Config.merge_defaults(options) 107 | opts = Gantree::Base.check_for_updates(opts) 108 | docker = Gantree::Docker.new(opts) 109 | docker.pull 110 | docker.build 111 | docker.push 112 | Gantree::Deploy.new(server,opts).run 113 | end 114 | 115 | map "-v" => :version 116 | desc "version", "gantree version" 117 | def version 118 | puts VERSION 119 | end 120 | 121 | end 122 | end 123 | 124 | -------------------------------------------------------------------------------- /lib/gantree/cli/help.rb: -------------------------------------------------------------------------------- 1 | module Gantree 2 | class CLI < Thor 3 | class Help 4 | class << self 5 | def init 6 | <<-EOL 7 | Examples: 8 | 9 | $ gantree init -u USERNAME -p PORT HANDLE/REPO:TAG 10 | 11 | $ gantree init -u frodriguez -p 3000 bleacher/cauldron:master 12 | EOL 13 | end 14 | 15 | def deploy 16 | <<-EOL 17 | Examples: 18 | 19 | $ gantree deploy -t TAG ENVIRONMENT 20 | 21 | $ gantree deploy -t latest stag-cauldon-app-s1 22 | 23 | # to deploy to all environments to within the same application 24 | 25 | $ gantree deploy -t TAG APPLICATION 26 | 27 | $ gantree deploy -t TAG cauldron-stag-s1 28 | 29 | # add remote .ebextensions 30 | 31 | $ gantree deploy -t TAG stag-cauldron-s1 -x "git@github.com:br/.ebextensions.git" 32 | 33 | # add remote .ebextensions branch 34 | 35 | $ gantree deploy -t TAG stag-cauldron-s1 -x "git@github.com:br/.ebextensions:feature_branch" 36 | 37 | EOL 38 | end 39 | 40 | def create 41 | <<-EOL 42 | Examples: 43 | 44 | $ gantree create APPLICATION 45 | 46 | $ gantree create linguist-stag-s1 47 | 48 | $ gantree create APPLICATION -e ENVIRONMENT 49 | 50 | $ gantree create linguist-stag-s1 -e linguist-stag-app-s1 51 | 52 | $ gantree create --dupe=rails-stag-s1 rails-stag-s3 53 | EOL 54 | end 55 | 56 | def update 57 | <<-EOL 58 | Examples: 59 | 60 | # Update a cloudformation stack 61 | 62 | $ gantree update linguist-stag-s1 63 | 64 | # Add an app role to an existing stack 65 | 66 | $ gantree update linguist-stag-s1 -r worker 67 | 68 | # Update docker solution starck version 69 | 70 | $ gantree update linguist-stag-s1 -s latest 71 | 72 | $ gantree update linguist-stag-s1 -s "64bit Amazon Linux 2014.09 v1.0.11 running Docker 1.3.3" 73 | EOL 74 | end 75 | 76 | def build 77 | <<-EOL 78 | Builds and tags a docker application. 79 | 80 | Examples: 81 | 82 | # Automatically tag a build 83 | 84 | $ gantree build 85 | 86 | # Add custom tag to a build 87 | 88 | $ gantree build -t deploy 89 | 90 | # Override image path to point to another hub 91 | 92 | $ gantree build -i quay.io/bleacherreport/cms 93 | 94 | EOL 95 | end 96 | 97 | def push 98 | <<-EOL 99 | Push docker image tag to hub 100 | 101 | Examples: 102 | 103 | # Push automatically tagged build 104 | 105 | $ gantree push 106 | 107 | # Push custom tagged build 108 | 109 | $ gantree push -t deploy 110 | 111 | # Push to another hub/acocunt/repo 112 | 113 | $ gantree push -i quay.io/bleacherreport/cms 114 | EOL 115 | end 116 | 117 | def ship 118 | <<-EOL 119 | build, push and deploy docker image to elastic beanstalk 120 | 121 | Examples: 122 | 123 | # Automatically tag a build, push that build and deploy to elastic beanstalk 124 | 125 | $ gantree ship cms-stag-s1 126 | 127 | # Override defaults 128 | 129 | $ gantree ship -i bleacher/cms -x "git@github.com:br/.ebextensions.git:master" cms-stag-s1 130 | 131 | $ gantree ship -i bleacher/cms -t built -x "git@github.com:br/.ebextensions.git:master" cms-stag-s1 132 | EOL 133 | end 134 | 135 | def restart 136 | <<-EOL 137 | Restart docker environment 138 | 139 | Examples: 140 | 141 | $ gantree restart stag-rails-app-s1 142 | EOL 143 | end 144 | end 145 | end 146 | end 147 | end -------------------------------------------------------------------------------- /lib/gantree/config.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' 2 | 3 | module Gantree 4 | class Config 5 | class << self 6 | def merge_defaults(options={}) 7 | configs = ["#{ENV['HOME']}/.gantreecfg",".gantreecfg"] 8 | hash = {} 9 | configs.each do |config| 10 | hash.merge!(merge_config(options,config)) if config_exists?(config) 11 | end 12 | Hash[hash.map{ |k, v| [k.to_sym, v] }] 13 | end 14 | 15 | def merge_config options, config 16 | defaults = JSON.parse(File.open(config).read) 17 | defaults.merge(options) 18 | end 19 | 20 | def config_exists? config 21 | File.exist? config 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/gantree/create.rb: -------------------------------------------------------------------------------- 1 | require 'cloudformation-ruby-dsl' 2 | require 'highline/import' 3 | require_relative 'cfn/master' 4 | require_relative 'cfn/beanstalk' 5 | require_relative 'cfn/resources' 6 | 7 | module Gantree 8 | class Create < Base 9 | attr_reader :env, :stack_name 10 | 11 | def initialize stack_name,options 12 | check_credentials 13 | set_aws_keys 14 | 15 | @requirements = "#!/usr/bin/env ruby 16 | require 'cloudformation-ruby-dsl/cfntemplate' 17 | require 'cloudformation-ruby-dsl/spotprice' 18 | require 'cloudformation-ruby-dsl/table'" 19 | 20 | additional_options = { 21 | requirements: @requirements, 22 | stack_name: stack_name, 23 | stack_hash: (0...8).map { (65 + rand(26)).chr }.join 24 | } 25 | @options = options.merge(additional_options) 26 | @options[:env] ||= create_default_env 27 | @options[:env_type] ||= env_type 28 | @options[:solution] ||= get_latest_docker_solution 29 | @templates = ['master','resources','beanstalk'] 30 | end 31 | 32 | def run 33 | @options[:rds_enabled] = rds_enabled? if @options[:rds] 34 | print_options 35 | create_cfn_if_needed 36 | create_all_templates unless @options[:local] 37 | upload_templates unless @options[:dry_run] 38 | create_aws_cfn_stack unless @options[:dry_run] 39 | end 40 | 41 | def stack_template 42 | s3.buckets["#{@options[:cfn_bucket]}/#{@options[:stack_name]}"].objects["#{@options[:stack_name]}-master.cfn.json"] 43 | end 44 | 45 | def create_cfn_if_needed 46 | Dir.mkdir 'cfn' unless File.directory?("cfn") 47 | end 48 | 49 | def create_all_templates 50 | @options[:dupe] ? duplicate_stack : generate_all_templates 51 | end 52 | 53 | def generate_all_templates 54 | puts "Generating templates from gantree" 55 | generate("master", MasterTemplate.new(@options).create) 56 | generate("beanstalk", BeanstalkTemplate.new(@options).create) 57 | generate("resources", ResourcesTemplate.new(@options).create) 58 | end 59 | 60 | def duplicate_stack 61 | puts "Duplicating cluster" 62 | orgin_stack_name = @options[:dupe] 63 | @templates.each do |template| 64 | FileUtils.cp("cfn/#{orgin_stack_name}-#{template}.cfn.json", "cfn/#{@options[:stack_name]}-#{template}.cfn.json") 65 | file = IO.read("cfn/#{@options[:stack_name]}-#{template}.cfn.json") 66 | file.gsub!(/#{escape_characters_in_string(orgin_stack_name)}/, @options[:stack_name]) 67 | replace_env_references(file) 68 | IO.write("cfn/#{@options[:stack_name]}-#{template}.cfn.json",file) 69 | end 70 | end 71 | 72 | def replace_env_references file 73 | origin_tags = @options[:dupe].split("-") 74 | new_tags = @options[:stack_name].split("-") 75 | possible_roles = ["app","worker","listener","djay","scheduler","sched","list","lisnr","listnr"] 76 | possible_roles.each do |role| 77 | origin_env = [origin_tags[1],origin_tags[0],role,origin_tags[2]].join('-') 78 | new_env = [new_tags[1],new_tags[0],role,new_tags[2]].join('-') 79 | file.gsub!(/#{escape_characters_in_string(origin_env)}/, new_env) 80 | end 81 | end 82 | 83 | def generate(template_name, template) 84 | IO.write("cfn/#{template_name}.rb", template) 85 | json = `ruby cfn/#{template_name}.rb expand` 86 | Dir.mkdir 'cfn' rescue Errno::ENOENT 87 | template_file_name = "#{@options[:stack_name]}-#{template_name}.cfn.json" 88 | IO.write("cfn/#{template_file_name}", json) 89 | puts "Created #{template_file_name} in the cfn directory" 90 | FileUtils.rm("cfn/#{template_name}.rb") 91 | end 92 | 93 | def create_aws_cfn_stack 94 | puts "Creating stack on aws..." 95 | stack = cfm.stacks.create(@options[:stack_name], stack_template, { 96 | :disable_rollback => true, 97 | :tags => [ 98 | { key: "StackName", value: @options[:stack_name] }, 99 | ]}) 100 | end 101 | 102 | def rds_enabled? 103 | if @options[:rds] == nil 104 | puts "RDS is not enabled, no DB created" 105 | false 106 | elsif @options[:rds] == "pg" || @rds == "mysql" 107 | puts "RDS is enabled, creating DB" 108 | true 109 | else 110 | raise "The --rds option you passed is not supported please use 'pg' or 'mysql'" 111 | end 112 | end 113 | end 114 | end 115 | 116 | -------------------------------------------------------------------------------- /lib/gantree/delete.rb: -------------------------------------------------------------------------------- 1 | require 'highline/import' 2 | require 'colorize' 3 | 4 | module Gantree 5 | class Delete < Base 6 | 7 | def initialize stack_name,options 8 | check_credentials 9 | set_aws_keys 10 | @stack_name = stack_name 11 | @options = options 12 | end 13 | 14 | def run input="" 15 | input = "y" if @options[:force] 16 | input ||= ask "Are you sure? (y|n)" 17 | if input == "y" || @options[:force] 18 | puts "Deleting stack from aws" 19 | return if @options[:dry_run] 20 | puts "Deleted".green if cfm.stacks[@stack_name].delete 21 | else 22 | puts "canceling...".yellow 23 | end 24 | end 25 | end 26 | end 27 | 28 | -------------------------------------------------------------------------------- /lib/gantree/deploy.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'archive/zip' 3 | require 'colorize' 4 | require 'librato/metrics' 5 | require_relative 'release_notes' 6 | require_relative 'wiki' 7 | require_relative 'notification' 8 | 9 | module Gantree 10 | class Deploy < Base 11 | attr_reader :name 12 | def initialize name, options 13 | check_credentials 14 | set_aws_keys 15 | @name = name 16 | @options = options 17 | @ext = @options[:ext] 18 | @dockerrun_file = "Dockerrun.aws.json" 19 | print_options 20 | end 21 | 22 | def run 23 | check_eb_bucket 24 | if application? 25 | DeployApplication.new(@name,@options).run 26 | elsif environment? 27 | puts "Found Environment: #{name}".green 28 | deploy([name]) 29 | else 30 | error_msg "You leave me with nothing to deploy".red 31 | end 32 | end 33 | 34 | def environment_found? 35 | @environments.length >=1 ? true : false 36 | end 37 | 38 | def deploy_to_one 39 | env = @environments.first[:environment_name] 40 | puts "Found Environment: #{env}".green 41 | deploy([env]) 42 | end 43 | 44 | def application? 45 | results = eb.describe_applications({ application_names: ["#{@name}"]}) 46 | results[:applications].length == 1 ? true : false 47 | end 48 | 49 | 50 | def environment? 51 | results = eb.describe_environments({ environment_names: ["#{@name}"]})[:environments] 52 | if results.length == 0 53 | puts "ERROR: Environment '#{name}' not found" 54 | exit 1 55 | else 56 | @app = results[0][:application_name] 57 | return true 58 | end 59 | end 60 | 61 | def deploy(envs) 62 | @envs = envs 63 | check_dir_name(envs) unless @options[:force] 64 | return if @options[:dry_run] 65 | version = DeployVersion.new(@options, envs[0]) 66 | @packaged_version = version.run 67 | puts "packaged_version: #{@packaged_version}" 68 | upload_to_s3 69 | create_eb_version 70 | update_application(envs) 71 | if @options[:slack] 72 | msg = "#{ENV['USER']} is deploying #{@packaged_version} to #{@app} #{@envs.inspect} gantree version #{Gantree::VERSION} " 73 | msg += "Tag: #{@options[:tag]}" if @options[:tag] 74 | Notification.new(@options[:slack]).say(msg) unless @options[:silent] 75 | end 76 | if @options[:librato] 77 | puts "Found Librato Key" 78 | Librato::Metrics.authenticate @options[:librato]["email"], @options[:librato]["token"] 79 | Librato::Metrics.annotate :deploys, "deploys",:source => "#{@app}", :start_time => Time.now.to_i 80 | puts "Librato metric submitted" 81 | end 82 | env = @envs.find {|e| e =~ /-app/ } 83 | if env && @options[:release_notes_wiki] && (prod_deploy? || @options[:release_notes_staging]) 84 | ReleaseNotes.new(@options[:release_notes_wiki], env, @packaged_version).create 85 | `git tag #{tag}` 86 | `git push --tags` 87 | end 88 | version.clean_up 89 | end 90 | 91 | def upload_to_s3 92 | puts "uploading #{@packaged_version} to #{set_bucket}" 93 | s3.buckets["#{set_bucket}"].objects["#{@app}-#{@packaged_version}"].write(:file => @packaged_version) 94 | end 95 | 96 | def create_eb_version 97 | begin 98 | eb.create_application_version({ 99 | :application_name => "#{@app}", 100 | :version_label => "#{@packaged_version}", 101 | :source_bundle => { 102 | :s3_bucket => "#{set_bucket}", 103 | :s3_key => "#{@app}-#{@packaged_version}" 104 | } 105 | }) 106 | rescue AWS::ElasticBeanstalk::Errors::InvalidParameterValue => e 107 | puts "No Application named #{@app} found #{e}" 108 | end 109 | end 110 | 111 | def update_application(envs) 112 | envs.each do |env| 113 | begin 114 | eb.update_environment({ 115 | :environment_name => env, 116 | :version_label => @packaged_version 117 | }) 118 | puts "Deployed #{@packaged_version} to #{env} on #{@app}".green 119 | rescue AWS::ElasticBeanstalk::Errors::InvalidParameterValue => e 120 | puts "Error: Something went wrong during the deploy to #{env}".red 121 | puts "#{e.message}" 122 | end 123 | end 124 | end 125 | 126 | def check_dir_name envs 127 | dir_name = File.basename(Dir.getwd) 128 | msg = "WARN: You are deploying from a repo that doesn't match #{@app}" 129 | puts msg.yellow if envs.any? { |env| env.include?(dir_name) } == false 130 | end 131 | 132 | def check_eb_bucket 133 | bucket = set_bucket 134 | puts "S3 Bucket: #{bucket}".light_blue 135 | s3.buckets.create(bucket) unless s3.buckets[bucket].exists? 136 | end 137 | 138 | def set_bucket 139 | if @options[:eb_bucket] 140 | bucket = @options[:eb_bucket] 141 | else 142 | bucket = generate_eb_bucket 143 | end 144 | end 145 | 146 | def generate_eb_bucket 147 | unique_hash = Digest::SHA1.hexdigest ENV['AWS_ACCESS_KEY_ID'] 148 | "eb-bucket-#{unique_hash}" 149 | end 150 | 151 | def prod_deploy? 152 | @envs.first.split("-").first == "prod" 153 | end 154 | end 155 | end 156 | 157 | -------------------------------------------------------------------------------- /lib/gantree/deploy_applications.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'archive/zip' 3 | require 'colorize' 4 | require_relative 'notification' 5 | 6 | module Gantree 7 | class DeployApplication < Deploy 8 | attr_accessor :options 9 | 10 | def initialize name, options 11 | @options = options 12 | @name = name 13 | puts "Found Application: #{@name}".green 14 | @environments = eb.describe_environments({ :application_name => @name })[:environments] 15 | end 16 | 17 | def run 18 | if multiple_environments? 19 | deploy_to_all 20 | elsif environment_found? 21 | deploy_to_one 22 | else 23 | error_msg "ERROR: There are no environments in this application" 24 | end 25 | end 26 | 27 | def multiple_environments? 28 | @environments.length > 1 ? true : false 29 | end 30 | 31 | def environment_found? 32 | @environments.length >=1 ? true : false 33 | end 34 | 35 | def deploy_to_all 36 | puts "WARN: Deploying to All Environments in the Application: #{@name}".yellow 37 | sleep 3 38 | envs = [] 39 | @environments.each do |env| 40 | envs << env[:environment_name] 41 | end 42 | puts "envs: #{envs}" 43 | deploy(envs) 44 | end 45 | end 46 | end 47 | 48 | -------------------------------------------------------------------------------- /lib/gantree/deploy_version.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'archive/zip' 3 | require 'colorize' 4 | require_relative 'notification' 5 | 6 | module Gantree 7 | class DeployVersion < Deploy 8 | 9 | attr_reader :packaged_version, :dockerrun_file 10 | def initialize options, env 11 | @options = options 12 | @ext = @options[:ext] 13 | @ext_role = @options[:ext_role] 14 | @project_root = @options[:project_root] 15 | @dockerrun_file = "#{@project_root}Dockerrun.aws.json" 16 | @env = env 17 | end 18 | 19 | def run 20 | @packaged_version = create_version_files 21 | end 22 | 23 | def docker 24 | @docker ||= JSON.parse(IO.read(@dockerrun_file)) 25 | end 26 | 27 | def set_auth 28 | docker["Authentication"] = {} 29 | items = @options[:auth].split("/") 30 | bucket = items.shift 31 | key = items.join("/") 32 | docker["Authentication"]["Bucket"] = bucket 33 | docker["Authentication"]["Key"] = key 34 | IO.write("/tmp/#{@dockerrun_file}", JSON.pretty_generate(docker)) 35 | end 36 | 37 | def set_tag_to_deploy 38 | image = docker["Image"]["Name"] 39 | token = image.split(":") 40 | if token.length == 3 41 | image = token[0] + (":") + token[1] + ":#{@options[:tag]}" 42 | docker["Image"]["Name"] = image 43 | elsif token.length == 2 44 | image.gsub!(/:(.*)$/, ":#{@options[:tag]}") 45 | else 46 | puts "Too many ':'".yellow 47 | end 48 | IO.write("/tmp/#{@dockerrun_file}", JSON.pretty_generate(docker)) 49 | end 50 | 51 | def set_image_path 52 | image = docker["Image"]["Name"] 53 | image.gsub!(/(.*):/, "#{@options[:image_path]}:") 54 | path = "/tmp/#{@dockerrun_file}" 55 | FileUtils.mkdir_p(File.dirname(path)) unless File.exist?(path) 56 | IO.write(path, JSON.pretty_generate(docker)) 57 | image 58 | end 59 | 60 | def version_tag 61 | @options[:tag] || tag 62 | end 63 | 64 | def create_version_files 65 | clean_up 66 | version = "#{version_tag}-#{Time.now.strftime("%m-%d-%Y-%H-%M-%S")}" 67 | puts "version: #{version}" 68 | set_auth if @options[:auth] 69 | set_image_path if @options[:image_path] 70 | set_tag_to_deploy 71 | if File.directory?(".ebextensions/") || @ext || @ext_role 72 | zip = "#{@project_root}#{version}.zip" 73 | merge_extensions 74 | puts "The following files are being zipped".yellow 75 | system('ls -l /tmp/merged_extensions/.ebextensions/') 76 | Archive::Zip.archive(zip, ['/tmp/merged_extensions/.ebextensions/', "/tmp/#{@dockerrun_file}"]) 77 | zip 78 | else 79 | new_dockerrun = "/tmp/#{version}-Dockerrun.aws.json" 80 | FileUtils.cp("/tmp/Dockerrun.aws.json", new_dockerrun) 81 | new_dockerrun 82 | end 83 | end 84 | 85 | def ext? 86 | if @ext 87 | true 88 | else 89 | false 90 | end 91 | end 92 | 93 | def repo? 94 | if @ext.include? "github" 95 | puts "Cloning: #{@ext}..." 96 | true 97 | else 98 | false 99 | end 100 | end 101 | 102 | def local_extensions? 103 | File.directory?(".ebextensions/") 104 | end 105 | 106 | def get_ext_repo repo 107 | if ext_branch? repo 108 | repo.sub(":#{get_ext_branch repo}", '') 109 | else 110 | repo 111 | end 112 | end 113 | 114 | def ext_branch? repo 115 | if repo.count(":") == 2 116 | true 117 | else 118 | false 119 | end 120 | end 121 | 122 | def get_ext_branch repo 123 | branch = repo.match(/:.*(:.*)$/)[1] 124 | branch.tr(':','') 125 | end 126 | 127 | def clone_repo repo 128 | repo_name = repo.split('/').last 129 | FileUtils.mkdir("/tmp/#{repo_name}") 130 | if ext_branch? repo 131 | `git clone -b #{get_ext_branch repo} #{get_ext_repo repo} /tmp/#{repo_name}/` 132 | else 133 | `git clone #{get_ext_repo repo} /tmp/#{repo_name}/` 134 | end 135 | FileUtils.cp_r "/tmp/#{repo_name}/.", "/tmp/merged_extensions/.ebextensions/" 136 | end 137 | 138 | def clean_up 139 | puts "Cleaning up tmp files".yellow 140 | FileUtils.rm_rf @packaged_version if @packaged_version 141 | FileUtils.rm_rf("/tmp/#{@ext.split('/').last}") if File.directory?("/tmp/#{@ext.split('/').last}") 142 | FileUtils.rm_rf("/tmp/#{@ext_role.split('/').last}:#{get_role_type}") if @ext_role 143 | FileUtils.rm_rf("/tmp/merged_extensions/") if File.directory? "/tmp/merged_extensions/" 144 | puts "All tmp files removed".green 145 | end 146 | 147 | def merge_extensions 148 | FileUtils.mkdir("/tmp/merged_extensions/") 149 | FileUtils.mkdir("/tmp/merged_extensions/.ebextensions/") 150 | clone_repo @ext if @ext 151 | clone_repo "#{@ext_role}:#{get_role_type}" if @ext_role 152 | FileUtils.cp_r('.ebextensions/.','/tmp/merged_extensions/.ebextensions') if File.directory? ".ebextensions/" 153 | end 154 | def get_role_type 155 | @env.split('-')[2] 156 | end 157 | end 158 | end 159 | 160 | -------------------------------------------------------------------------------- /lib/gantree/docker.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' 2 | 3 | module Gantree 4 | class Docker < Base 5 | 6 | def initialize options 7 | check_credentials 8 | set_aws_keys 9 | @options = options 10 | @image_path = @options[:image_path] 11 | @image_path||= get_image_path 12 | @tag = @options[:tag] ||= tag 13 | @base_image_tag = @options[:base_image_tag] 14 | end 15 | 16 | def get_image_path 17 | dockerrun = JSON.parse(IO.read("Dockerrun.aws.json")) 18 | image = dockerrun["Image"]["Name"] 19 | image.gsub!(/:(.*)$/, "") if image.include? ":" 20 | puts "Image Path: #{image}".light_blue 21 | image 22 | end 23 | 24 | def pull 25 | puts "Pulling Image First..." 26 | if @base_image_tag 27 | puts "Pulled Image: #{@image_path}:#{@base_image_tag}".green if system("docker pull #{@image_path}:#{@base_image_tag}") 28 | elsif system("docker pull #{@image_path}") 29 | puts "Pulled Image: #{@image_path}".green 30 | else 31 | puts "Failed to Pull Image #{@image_path}".red 32 | end 33 | end 34 | 35 | def build 36 | puts "Building..." 37 | 38 | if system("git rev-parse --short HEAD > version.txt") 39 | puts "Outputting short hash to version.txt" 40 | else 41 | puts "Error: Could not output commit hash to version.txt (is this a git repository?)" 42 | end 43 | 44 | if system("docker build -t #{@image_path}:#{@tag} .") 45 | puts "Image Built: #{@image_path}:#{@tag}".green 46 | puts "gantree push --image-path #{@image_path} -t #{@tag}" unless @options[:hush] 47 | puts "gantree deploy app_name -t #{@tag}" unless @options[:hush] 48 | else 49 | puts "Error: Image was not built successfully".red 50 | exit 1 51 | end 52 | 53 | if system("rm -f version.txt") 54 | puts "Removing version.txt after docker build" 55 | else 56 | puts "Error: Can't remove version.txt after docker build" 57 | end 58 | end 59 | 60 | def push 61 | puts "Pushing to #{@image_path}:#{@tag} ..." 62 | if system("docker push #{@image_path}:#{@tag}") 63 | puts "Image Pushed: #{@image_path}:#{@tag}".green 64 | puts "gantree deploy app_name -t #{@tag}" unless @options[:hush] 65 | else 66 | puts "Error: Image was not pushed successfully".red 67 | exit 1 68 | end 69 | end 70 | end 71 | end 72 | 73 | -------------------------------------------------------------------------------- /lib/gantree/init.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'aws-sdk-v1' 3 | 4 | module Gantree 5 | class Init < Base 6 | attr_reader :image, :options, :bucket_name 7 | 8 | def initialize image, options 9 | check_credentials 10 | set_aws_keys 11 | 12 | @image = image 13 | @options = options 14 | @bucket_name = @options.bucket || default_bucket_name 15 | end 16 | 17 | def run 18 | puts "initialize image #{@image}" 19 | print_options 20 | 21 | FileUtils.rm("Dockerrun.aws.json") if File.exist?("Dockerrun.aws.json") 22 | create_docker_config_s3_bucket unless options[:dry_run] 23 | create_dockerrun 24 | upload_docker_config if options.user && !options[:dry_run] 25 | end 26 | 27 | private 28 | def default_bucket_name 29 | [@options.user, "docker", "cfgs"].compact.join("-") 30 | end 31 | 32 | def create_docker_config_s3_bucket 33 | bucket = s3.buckets.create(bucket_name) unless s3.buckets[bucket_name].exists? 34 | end 35 | 36 | def dockerrun_object 37 | docker = { 38 | AWSEBDockerrunVersion: "1", 39 | Image: { 40 | Name: image, 41 | Update: true 42 | }, 43 | Logging: "/var/log/nginx", 44 | Ports: [ 45 | { 46 | ContainerPort: options.port 47 | } 48 | ] 49 | } 50 | if options.user 51 | docker["Authentication"] = { 52 | Bucket: bucket_name, 53 | Key: "#{options.user}.dockercfg" 54 | } 55 | end 56 | docker 57 | end 58 | 59 | def create_dockerrun 60 | IO.write("Dockerrun.aws.json", JSON.pretty_generate(dockerrun_object)) 61 | end 62 | 63 | def upload_docker_config 64 | raise "You need to run 'docker login' to generate a .dockercfg file" unless dockercfg_file_exist? 65 | filename = "#{ENV['HOME']}/#{options.user}.dockercfg" 66 | FileUtils.cp("#{ENV['HOME']}/.dockercfg", "#{ENV['HOME']}/#{options.user}.dockercfg") 67 | key = File.basename(filename) 68 | s3.buckets[bucket_name].objects[key].write(:file => filename) 69 | end 70 | 71 | def dockercfg_file_exist? 72 | File.exist?("#{ENV['HOME']}/.dockercfg") 73 | end 74 | end 75 | end 76 | 77 | -------------------------------------------------------------------------------- /lib/gantree/notification.rb: -------------------------------------------------------------------------------- 1 | require 'slackr' 2 | class Notification 3 | 4 | def initialize(options={}) 5 | @slack = Slackr.connect(options["team"], options["token"]) 6 | end 7 | 8 | def say(msg) 9 | @slack.say(msg) 10 | end 11 | end 12 | 13 | -------------------------------------------------------------------------------- /lib/gantree/release_notes.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk' 2 | 3 | module Gantree 4 | class ReleaseNotes 5 | attr_reader :current_sha 6 | attr_writer :beanstalk 7 | def initialize(wiki, env_name, packaged_version) 8 | @env_name = env_name 9 | @wiki = wiki 10 | @org = wiki.split("/")[0..-2].join("/") 11 | @packaged_version = packaged_version 12 | end 13 | 14 | def current_sha 15 | @packaged_version.split("-")[2] 16 | end 17 | 18 | def beanstalk 19 | return @beanstalk if @beanstalk 20 | @beanstalk = Aws::ElasticBeanstalk::Client.new( 21 | :region => ENV['AWS_REGION'] || "us-east-1" 22 | ) 23 | end 24 | 25 | def environment 26 | beanstalk.describe_environments(:environment_names => [@env_name]).environments.first 27 | end 28 | 29 | def previous_tag 30 | environment.version_label 31 | end 32 | 33 | def previous_sha 34 | if previous_tag 35 | previous_tag.split("-")[2] 36 | else 37 | # for edge case where release notes have never been generated before, just use 1 commit before the current one 38 | "#{current_sha}~1" 39 | end 40 | end 41 | 42 | def app_name 43 | name = environment.application_name 44 | name.include?("-") ? name.split("-")[0] : name # TODO: business logic 45 | end 46 | 47 | def pacific_time 48 | Time.now.strftime '%Y-%m-%d %a %I:%M%p PDT' 49 | end 50 | 51 | def commits 52 | return @commits if @commits 53 | # Get commits for this release 54 | commits = git_log 55 | commits = commits.split("COMMIT_SEPARATOR") 56 | commits = commits. 57 | collect {|x| x.strip }. 58 | reject {|x| x.empty? }. 59 | collect {|x| x.gsub(/\n+/, ", ")} 60 | tickets = [] 61 | commits.each do |msg| 62 | md = msg.match(/(\w+-\d+)/) 63 | if md 64 | ticket_id = md[1] 65 | tickets << msg unless tickets.detect {|t| t =~ Regexp.new("^#{ticket_id}") } 66 | else 67 | tickets << msg 68 | end 69 | end 70 | tickets 71 | end 72 | 73 | def commits_list 74 | commits.collect{|x| "* #{x}" }.join("\n") 75 | end 76 | 77 | def git_log 78 | execute("git log --no-merges --pretty=format:'%B COMMIT_SEPARATOR' #{previous_sha}..#{current_sha}").strip 79 | end 80 | 81 | def execute(cmd) 82 | `#{cmd}` 83 | end 84 | 85 | def notes 86 | compare = "#{previous_sha}...#{current_sha}" 87 | notes = <<-EOL 88 | #{@env_name} #{pacific_time} [#{compare}](#{@org}/#{app_name}/compare/#{compare}) by #{ENV['USER']} (#{@packaged_version}) 89 | #{commits_list} 90 | EOL 91 | end 92 | 93 | def create 94 | filename = "Release-notes-br-#{app_name}.md" # business logic 95 | Gantree::Wiki.new(notes, filename, @wiki).update 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/gantree/update.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' 2 | 3 | module Gantree 4 | class Update < Base 5 | 6 | def initialize stack_name,options 7 | check_credentials 8 | set_aws_keys 9 | @options = options 10 | @options[:stack_name] = stack_name 11 | @options[:env] ||= create_default_env 12 | @options[:env_type] ||= env_type 13 | @templates = ['master','resources','beanstalk'] 14 | end 15 | 16 | def run 17 | puts "Updating stack from local cfn repo" 18 | add_role @options[:role] if @options[:role] 19 | change_solution_stack if @options[:solution] 20 | return if @options[:dry_run] 21 | upload_templates 22 | puts "Stack Updated".green if cfm.stacks[@options[:stack_name]].update(:template => stack_template) 23 | end 24 | 25 | def stack_template 26 | s3.buckets["#{@options[:cfn_bucket]}/#{@stack_name}"].objects["#{@stack_name}-master.cfn.json"] 27 | end 28 | 29 | def change_solution_stack 30 | beanstalk = JSON.parse(IO.read("cfn/#{@options[:stack_name]}-beanstalk.cfn.json")) 31 | solution_stack = set_solution_stack 32 | beanstalk["Resources"]["ConfigurationTemplate"]["Properties"]["SolutionStackName"] = solution_stack 33 | beanstalk["Resources"]["ConfigurationTemplate"]["Properties"]["Description"] = solution_stack 34 | IO.write("cfn/#{@options[:stack_name]}-beanstalk.cfn.json",JSON.pretty_generate(beanstalk)) 35 | puts "Updated solution to #{solution_stack}".green 36 | end 37 | 38 | def set_solution_stack 39 | @options[:solution] == "latest" ? get_latest_docker_solution : @options[:solution] 40 | end 41 | 42 | def add_role name 43 | env = @options[:env].sub('app', name) 44 | beanstalk = JSON.parse(IO.read("cfn/#{@options[:stack_name]}-beanstalk.cfn.json")) 45 | unless beanstalk["Resources"][name] then 46 | role = { 47 | "Type" => "AWS::ElasticBeanstalk::Environment", 48 | "Properties"=> { 49 | "ApplicationName" => "#{@options[:stack_name]}", 50 | "EnvironmentName" => "#{env}", 51 | "Description" => "#{name} Environment", 52 | "TemplateName" => { 53 | "Ref" => "ConfigurationTemplate" 54 | }, 55 | "OptionSettings" => [] 56 | } 57 | } 58 | #puts JSON.pretty_generate role 59 | beanstalk["Resources"]["#{name}".to_sym] = role 60 | IO.write("cfn/#{@options[:stack_name]}-beanstalk.cfn.json", JSON.pretty_generate(beanstalk)) unless @options[:dry_run] 61 | puts JSON.pretty_generate(beanstalk["Resources"].to_a.last) 62 | puts "Added new #{name} role".green 63 | else 64 | puts "Role already exists".red 65 | end 66 | end 67 | end 68 | end 69 | 70 | -------------------------------------------------------------------------------- /lib/gantree/version.rb: -------------------------------------------------------------------------------- 1 | module Gantree 2 | VERSION = "0.6.18" 3 | end 4 | -------------------------------------------------------------------------------- /lib/gantree/wiki.rb: -------------------------------------------------------------------------------- 1 | module Gantree 2 | class Wiki 3 | attr_reader :wiki_path 4 | attr_accessor :file_path 5 | def initialize(notes, filename, wiki_url, path='/tmp/') 6 | @notes = notes 7 | @wiki_url = wiki_url 8 | wiki_path = wiki_url.split("/").last.sub(".git","") 9 | @wiki_path = "#{path}#{wiki_path}" 10 | @file_path = "#{@wiki_path}/#{filename}" 11 | end 12 | 13 | def update 14 | pull 15 | added = add_to_top 16 | push if added 17 | end 18 | 19 | def pull 20 | puts "Updating wiki cached repo".colorize(:yellow) 21 | if File.exist?(@wiki_path) 22 | Dir.chdir(@wiki_path) do 23 | execute("git checkout master") unless execute("git branch").include?("* master") 24 | execute("git pull origin master") 25 | end 26 | else 27 | dirname = File.dirname(@wiki_path) 28 | FileUtils.mkdir_p(dirname) unless File.exist?(dirname) 29 | cmd = "cd #{dirname} && git clone #{@wiki_url}" 30 | execute(cmd) 31 | end 32 | end 33 | 34 | def add_to_top 35 | data = IO.read(@file_path) if File.exist?(@file_path) 36 | File.open(@file_path, "w") do |file| 37 | file.write(@notes) 38 | file.write("\n") 39 | file.write(data) if data 40 | end 41 | true 42 | end 43 | 44 | def push 45 | Dir.chdir(@wiki_path) do 46 | basename = File.basename(@file_path) 47 | execute("git add #{basename}") 48 | execute(%Q|git commit -m "Update release notes"|) 49 | execute("git push origin master") 50 | end 51 | end 52 | 53 | def execute(cmd) 54 | `#{cmd}` 55 | end 56 | end 57 | end -------------------------------------------------------------------------------- /packaging/bundler-config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: . 2 | BUNDLE_WITHOUT: development 3 | BUNDLE_DISABLE_SHARED_GEMS: '1' 4 | -------------------------------------------------------------------------------- /packaging/wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Figure out where this script is located. 5 | SELFDIR="`readlink -f /usr/bin/gantree`" 6 | SELFDIR=`dirname $SELFDIR` 7 | 8 | # Tell Bundler where the Gemfile and gems are. 9 | export BUNDLE_GEMFILE="$SELFDIR/lib/vendor/Gemfile" 10 | unset BUNDLE_IGNORE_CONFIG 11 | 12 | # Run the actual app using the bundled Ruby interpreter, with Bundler activated. 13 | eval "$SELFDIR/lib/ruby/bin/ruby" -rbundler/setup -rreadline "$SELFDIR/app/bin/gantree $@" 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/project/Dockerrun.aws.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSEBDockerrunVersion": "1", 3 | "Image": { 4 | "Name": "bleacher/carburetor", 5 | "Update": true 6 | }, 7 | "Logging": "/var/log/nginx", 8 | "Ports": [ 9 | { 10 | "ContainerPort": "300" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /spec/lib/gantree/1_release_notes_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gantree::ReleaseNotes do 4 | before(:all) do 5 | @wiki = "https://github.com/br/dev.wiki.git" 6 | @env_name = "stag-rails-app-s1" 7 | @packaged_version = "br-master-d961c96-06-17-2015-23-33-25.zip" 8 | @rn = Gantree::ReleaseNotes.new(@wiki, @env_name, @packaged_version) 9 | @rn.beanstalk = Aws::ElasticBeanstalk::Client.new(stub_responses: true) 10 | end 11 | 12 | def mock_environment 13 | double( 14 | version_label: "br-master-fb3e1cd-23.zip", 15 | application_name: "rails" 16 | ) 17 | end 18 | 19 | it "can retrieve the latest deployed master tag" do 20 | allow(@rn).to receive(:environment).and_return(mock_environment) 21 | expect(@rn.previous_tag).to include "br-master" 22 | end 23 | 24 | it "can retrieve the latest deployed sha from the master tag" do 25 | allow(@rn).to receive(:environment).and_return(mock_environment) 26 | expect(@rn.previous_sha).to eq "fb3e1cd" 27 | end 28 | 29 | it "can retrieve the current sha" do 30 | expect(@rn.current_sha).to eq "d961c96" 31 | end 32 | 33 | it "should generate notes" do 34 | allow(@rn).to receive(:environment).and_return(mock_environment) 35 | allow(@rn).to receive(:git_log).and_return(git_log_mock) 36 | notes = @rn.notes 37 | puts notes if ENV['DEBUG'] 38 | expect(notes).to include(@env_name) 39 | expect(notes).to include(@rn.pacific_time) 40 | expect(notes).to include("compare") 41 | expect(notes).to include("test up controller") 42 | end 43 | 44 | it "should grab commit messages" do 45 | allow(@rn).to receive(:git_log).and_return(git_log_mock) 46 | commits = @rn.commits 47 | p commits if ENV['DEBUG'] 48 | expect(commits).to be_a(Array) 49 | expect(commits).to include("commit 1") 50 | expect(commits).to include("commit 2") 51 | end 52 | 53 | it "should dedup the duplicate tickets from notes" do 54 | git_log_mock = <<-EOL 55 | CMS-296 sortable tweaks 56 | COMMIT_SEPARATOR 57 | CMS-296 tweak test to match implementation 58 | COMMIT_SEPARATOR 59 | CMS-423 Edit Content modal waits for Preview object to be fetched 60 | COMMIT_SEPARATOR 61 | CMS-423 WIP PreviewView area read from/write to area of tracks' embed code 62 | COMMIT_SEPARATOR 63 | EOL 64 | 65 | allow(@rn).to receive(:git_log).and_return(git_log_mock) 66 | commits = @rn.commits 67 | p commits if ENV['DEBUG'] 68 | expect(commits).to be_a(Array) 69 | expect(commits.size).to eq 2 70 | expect(commits).to include("CMS-296 sortable tweaks") 71 | expect(commits).to include("CMS-423 Edit Content modal waits for Preview object to be fetched") 72 | expect(commits).to_not include("CMS-423 WIP PreviewView area read from/write to area of tracks' embed code") 73 | end 74 | 75 | it "should remove newlines from each commit" do 76 | git_log_mock = <<-EOL 77 | CMS-296 sortable tweaks 78 | 79 | Commit with a newline 80 | COMMIT_SEPARATOR 81 | CMS-296 tweak test to match implementation 82 | COMMIT_SEPARATOR 83 | CMS-423 Edit Content modal waits for Preview object to be fetched 84 | COMMIT_SEPARATOR 85 | CMS-423 WIP PreviewView area read from/write to area of tracks' embed code 86 | COMMIT_SEPARATOR 87 | EOL 88 | 89 | allow(@rn).to receive(:git_log).and_return(git_log_mock) 90 | commits = @rn.commits 91 | p commits if ENV['DEBUG'] 92 | expect(commits).to include("CMS-296 sortable tweaks, Commit with a newline") 93 | expect(commits).to include("CMS-423 Edit Content modal waits for Preview object to be fetched") 94 | end 95 | end -------------------------------------------------------------------------------- /spec/lib/gantree/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # to run specs with what's remembered from vcr 4 | # $ rake 5 | # 6 | # to run specs with new fresh data from aws api calls 7 | # $ rake clean:vcr ; time rake 8 | describe Gantree::CLI do 9 | before(:all) do 10 | 11 | @app = "stag-carburetor-app-s2" 12 | @env = "carburetor" 13 | @owner = "bleacher" 14 | @repo = "cauldron" 15 | @tag = "master" 16 | @user = "feelobot" 17 | end 18 | 19 | describe "init" do 20 | it "should create a new dockerrun for a private repo" do 21 | out = execute("bin/gantree init -u #{@user} #{@owner}/#{@repo}:#{@tag} --dry-run") 22 | expect(out).to include "initialize image" 23 | expect(File.exist?("Dockerrun.aws.json")).to be true 24 | expect(IO.read("Dockerrun.aws.json").include? @user) 25 | end 26 | 27 | it "should create a new dockerrun for a public repo" do 28 | out = execute("bin/gantree init #{@owner}/#{@repo}:#{@tag} --dry-run") 29 | expect(out).to include "initialize image" 30 | expect(File.exist?("Dockerrun.aws.json")).to be true 31 | end 32 | 33 | it "specifies the bucket" do 34 | out = execute("bin/gantree init -u #{@user} -b docker-cfgs #{@owner}/#{@repo}:#{@tag} --dry-run") 35 | expect(out).to include "bucket: docker-cfgs" 36 | end 37 | end 38 | 39 | describe "deploy" do 40 | it "should deploy images" do 41 | execute("bin/gantree init #{@owner}/#{@repo}:#{@tag} --dry-run") 42 | out = execute("bin/gantree deploy #{@env} --dry-run --eb-bucket br-eb-versions --silent") 43 | expect(out).to include("Found Application: #{@env}") 44 | expect(out).to include("silent: silent") 45 | end 46 | 47 | it "should deploy images with remote extensions" do 48 | out = execute("bin/gantree deploy #{@app} -x 'git@github.com:br/.ebextensions' --eb-bucket br-eb-versions --dry-run --silent") 49 | expect(out).to include("Found Environment: #{@app}") 50 | expect(out).to include(".ebextensions") 51 | expect(out).to include("silent: silent") 52 | end 53 | 54 | it "should deploy images with remote extensions on a branch" do 55 | out = execute("bin/gantree deploy #{@env} -x 'git@github.com:br/.ebextensions:basic' --eb-bucket br_eb_versions --dry-run --silent") 56 | expect(out).to include("Found Application: #{@env}") 57 | expect(out).to include(".ebextensions:basic") 58 | expect(out).to include("silent: silent") 59 | end 60 | 61 | it "should notify slack of deploys" do 62 | out = execute("bin/gantree deploy #{@env} --eb-bucket br_eb_versions --dry-run") 63 | expect(out).to include("Found Application: #{@env}") 64 | end 65 | end 66 | =begin 67 | describe "create" do 68 | it "should create clusters" do 69 | out = execute("bin/gantree create #{@env} --dry-run --cfn-bucket templates") 70 | expect(out).to include "instance_size: m3.medium" 71 | expect(out).to include "stack_name: #{@env}" 72 | expect(out).to include "cfn_bucket: templates" 73 | end 74 | 75 | it "should create clusters with any docker version" do 76 | out = execute("bin/gantree create #{@env} --dry-run --solution '64bit Amazon Linux 2014.03 v1.0.1 running Docker 1.0.0' --cfn-bucket template") 77 | expect(out).to include "solution: 64bit Amazon Linux 2014.03 v1.0.1 running Docker 1.0.0" 78 | end 79 | 80 | it "should create clusters with databases" do 81 | out = execute("bin/gantree create #{@env} --dry-run --rds pg --cfn-bucket template") 82 | expect(out).to include "rds: pg" 83 | expect(out).to include "rds_enabled: true" 84 | end 85 | 86 | it "should create dupliacte clusters from local cfn" do 87 | out = execute("bin/gantree create #{@new_env} --dupe #{@env} --dry-run --cfn-bucket template") 88 | expect(out).to include "dupe: #{@env}" 89 | end 90 | end 91 | 92 | describe "update" do 93 | it "should update existing clusters" do 94 | out = execute("bin/gantree update #{@env} --dry-run --cfn-bucket template") 95 | expect(out).to include "Updating" 96 | end 97 | end 98 | 99 | describe "delete" do 100 | it "should update existing clusters" do 101 | out = execute("bin/gantree delete #{@env} --dry-run --force") 102 | expect(out).to include "Deleting" 103 | end 104 | end 105 | =end 106 | describe "#version" do 107 | it "should output gantree version" do 108 | out = execute("bin/gantree version") 109 | expect(out).to match /\d\.\d\.\d/ 110 | end 111 | 112 | it "should output gantree version using alias" do 113 | out = execute("bin/gantree -v") 114 | expect(out).to match /\d\.\d\.\d/ 115 | end 116 | end 117 | end 118 | 119 | -------------------------------------------------------------------------------- /spec/lib/gantree/deploy_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "pry" 3 | 4 | describe Gantree::Deploy do 5 | before(:all) do 6 | 7 | @stack_name = "knarr-stag-s1" 8 | @env = "stag-knarr-app-s1" 9 | @owner = "bleacher" 10 | @repo = "cauldron" 11 | @tag = "master" 12 | @user = "feelobot" 13 | end 14 | 15 | it "returns branch name of repo url" do 16 | options = { ext: "git@github.com:br/.ebextensions:basic" } 17 | expect(Gantree::DeployVersion.new(options, @env).send(:get_ext_branch,options[:ext])).to eq "basic" 18 | end 19 | 20 | it "returns just the repo url" do 21 | options = { ext: "git@github.com:br/.ebextensions:basic" } 22 | expect(Gantree::DeployVersion.new(options,@env).send(:get_ext_repo, options[:ext])).to eq "git@github.com:br/.ebextensions" 23 | end 24 | 25 | it "AWS gets the correct keys" do 26 | ENV['AWS_ACCESS_KEY_ID'] = 'FAKE_AWS_ACCESS_KEY' 27 | ENV['AWS_SECRET_ACCESS_KEY'] = 'FAKE_AWS_SECRET_ACCESS_KEY' 28 | gd = Gantree::Deploy.new( 29 | "image_name", 30 | :env => "cauldron-stag-s1" 31 | ) 32 | expect(AWS).to receive(:config).with( 33 | :access_key_id => 'FAKE_AWS_ACCESS_KEY', 34 | :secret_access_key => 'FAKE_AWS_SECRET_ACCESS_KEY' 35 | ) 36 | gd.set_aws_keys 37 | end 38 | 39 | it "the image path is dynamic" do 40 | options = { image_path: "quay.io/bleacherreport/cms" } 41 | deploy = Gantree::Deploy.new(@env,options) 42 | image_path = deploy.instance_variable_get("@options")[:image_path] 43 | expect(image_path).to eq("quay.io/bleacherreport/cms") 44 | end 45 | 46 | it "raises an error when no aws keys in ENV" do 47 | ENV['AWS_ACCESS_KEY_ID'] = nil 48 | ENV['AWS_SECRET_ACCESS_KEY'] = nil 49 | expect{gd = Gantree::Deploy.new( 50 | "image_name", 51 | :env => "cauldron-stag-s1" 52 | )}.to raise_error(RuntimeError) 53 | end 54 | end 55 | 56 | def dockerrun 57 | ' 58 | { 59 | "AWSEBDockerrunVersion": "1", 60 | "Image": { 61 | "Name": "bleacher/cms", 62 | "Update": true 63 | }, 64 | "Logging": "/var/log/nginx", 65 | "Ports": [ 66 | { 67 | "ContainerPort": "300" 68 | } 69 | ] 70 | } 71 | ' 72 | end 73 | -------------------------------------------------------------------------------- /spec/lib/gantree/deploy_version_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gantree::DeployVersion do 4 | before(:all) do 5 | end 6 | 7 | after(:all) do 8 | FileUtils.rm_f(Dir.glob("spec/fixtures/project/*.zip")) 9 | end 10 | 11 | it "should create package version" do 12 | @options = { 13 | :ext => "git@github.com:br/.ebextensions.git", 14 | :tag => "br-develop_mkemnitz-30b9bf6", 15 | :image_path => "bleacher/carburetor", 16 | :project_root => "spec/fixtures/project/" 17 | } 18 | @env = "stag-carburetor-app-s1" 19 | @version = Gantree::DeployVersion.new(@options, @env) 20 | packaged_version = @version.run 21 | expect(File.exist?(packaged_version)).to be true 22 | end 23 | 24 | it "should add auth info" do 25 | @options = { 26 | :ext => "git@github.com:br/.ebextensions.git", 27 | :tag => "br-develop_mkemnitz-30b9bf6", 28 | :image_path => "bleacher/carburetor", 29 | :project_root => "spec/fixtures/project/", 30 | :auth => "docker-cfgs/brops.dockercfg" 31 | } 32 | @env = "stag-carburetor-app-s1" 33 | @version = Gantree::DeployVersion.new(@options, @env) 34 | @version.set_auth 35 | data = JSON.parse(IO.read("/tmp/#{@version.dockerrun_file}")) 36 | expect(data["Authentication"]["Bucket"]).to eq "docker-cfgs" 37 | expect(data["Authentication"]["Key"]).to eq "brops.dockercfg" 38 | end 39 | 40 | end -------------------------------------------------------------------------------- /spec/lib/gantree/docker_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "pry" 3 | 4 | describe Gantree::Docker do 5 | before(:all) do 6 | ENV['AWS_ACCESS_KEY_ID'] = 'FAKE_AWS_ACCESS_KEY' 7 | ENV['AWS_SECRET_ACCESS_KEY'] = 'FAKE_AWS_SECRET_ACCESS_KEY' 8 | end 9 | 10 | it "requires an image_path option to be set" 11 | it "generates a unique tag for you" 12 | it "allows you to set your own tag to build" 13 | it "allows you to set your own tag to push" 14 | it "builds dockerfiles with tag" 15 | it "accepts image path parameter" 16 | it "pushes tag to dockerhub" 17 | 18 | end 19 | 20 | -------------------------------------------------------------------------------- /spec/lib/gantree/init_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "pry" 3 | 4 | describe Gantree::Init do 5 | before(:all) do 6 | ENV['AWS_ACCESS_KEY_ID'] = 'FAKE_AWS_ACCESS_KEY' 7 | ENV['AWS_SECRET_ACCESS_KEY'] = 'FAKE_AWS_SECRET_ACCESS_KEY' 8 | 9 | ENV["HOME"] = "/Users/gantree_user" 10 | 11 | @options = Thor::CoreExt::HashWithIndifferentAccess.new( 12 | "port" => "3000", 13 | "user" => "gantree_user", 14 | "bucket" => "bucket314159" 15 | ) 16 | 17 | @s3_bucket = AWS::S3::Bucket.new("bucket314159") 18 | end 19 | 20 | it "initializes the variables properly" do 21 | gi = Gantree::Init.new("bleacher/cauldron:master", @options) 22 | 23 | expect(gi.image).to eq("bleacher/cauldron:master") 24 | expect(gi.options.port).to eq("3000") 25 | end 26 | 27 | it "AWS gets the correct keys" do 28 | gi = Gantree::Init.new("image_name", @options) 29 | expect(AWS).to receive(:config).with( 30 | :access_key_id => 'FAKE_AWS_ACCESS_KEY', 31 | :secret_access_key => 'FAKE_AWS_SECRET_ACCESS_KEY' 32 | ) 33 | gi.set_aws_keys 34 | end 35 | 36 | it "uses default bucket_name" do 37 | options_no_bucket_name = Thor::CoreExt::HashWithIndifferentAccess.new( 38 | "port" => "3000", 39 | "user" => "gantree_user" 40 | ) 41 | gi = Gantree::Init.new("image_name", options_no_bucket_name) 42 | expect(gi.bucket_name).to eq("gantree_user-docker-cfgs") 43 | 44 | options_no_bucket_name_or_user = Thor::CoreExt::HashWithIndifferentAccess.new( 45 | "port" => "3000" 46 | ) 47 | gi2 = Gantree::Init.new("image_name", options_no_bucket_name_or_user) 48 | expect(gi2.bucket_name).to eq("docker-cfgs") 49 | end 50 | 51 | it "generates a dockerrun object" do 52 | gi = Gantree::Init.new("bleacher/cauldron:master", @options) 53 | 54 | dro = gi.send(:dockerrun_object) 55 | expect(dro).to eq( 56 | :AWSEBDockerrunVersion=>"1", 57 | :Image=>{:Name=>"bleacher/cauldron:master", :Update=>true}, 58 | :Logging=>"/var/log/nginx", 59 | :Ports=>[{:ContainerPort=>"3000"}], 60 | "Authentication"=>{:Bucket=>"bucket314159", :Key=>"gantree_user.dockercfg"} 61 | ) 62 | end 63 | 64 | it "creates docker config folder when s3 bucket already exists" do 65 | gi = Gantree::Init.new("bleacher/cauldron:master", @options) 66 | AWS::S3::BucketCollection.any_instance.stub(:[]).with(anything()) {Existence} 67 | AWS::S3::BucketCollection.stub(:create) {"OK"} 68 | 69 | expect(gi.send(:create_docker_config_s3_bucket)).to eq(nil) 70 | end 71 | 72 | it "creates docker config folder when s3 bucket doesnt not exist" do 73 | gi = Gantree::Init.new("bleacher/cauldron:master", @options) 74 | AWS::S3::BucketCollection.any_instance.stub(:[]).with(anything()) {NonExistence} 75 | AWS::S3::BucketCollection.any_instance.stub(:create).with(@options.bucket) {@s3_bucket} 76 | 77 | expect(gi.send(:create_docker_config_s3_bucket)).to eq(@s3_bucket) 78 | end 79 | 80 | it "uploads docker config to s3" do 81 | gi = Gantree::Init.new("bleacher/cauldron:master", @options) 82 | 83 | Gantree::Init.any_instance.stub(:dockercfg_file_exist?) { true } 84 | FileUtils.stub(:cp).with( 85 | "/Users/gantree_user/.dockercfg", 86 | "/Users/gantree_user/gantree_user.dockercfg" 87 | ) {"OK"} 88 | AWS::S3::S3Object.any_instance.stub(:write).with( 89 | :file => "/Users/gantree_user/gantree_user.dockercfg" 90 | ) {"OK"} 91 | 92 | expect(gi.send(:upload_docker_config)).to eq("OK") 93 | end 94 | end 95 | 96 | -------------------------------------------------------------------------------- /spec/lib/gantree/wiki_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gantree::Wiki do 4 | before(:all) do 5 | @notes = "fake_release_notes" 6 | filename = "Release-notes-br-rails.md" 7 | wiki = "git@github.com:br/dev.wiki.git" 8 | @wiki = Gantree::Wiki.new(@notes, filename, wiki, "tmp/") 9 | end 10 | 11 | def fake_clone_dir 12 | FileUtils.mkdir_p(@wiki.wiki_path) 13 | end 14 | 15 | it "should clone and pull" do 16 | FileUtils.rm_rf(@wiki.wiki_path) if ENV['CLEAN'] 17 | allow(@wiki).to receive(:execute).and_return(fake_clone_dir) 18 | @wiki.pull 19 | folder_created = File.exist?(@wiki.wiki_path) 20 | # puts "@wiki.wiki_path #{@wiki.wiki_path.inspect}" 21 | expect(folder_created).to be true 22 | @wiki.pull 23 | end 24 | 25 | it "should add notes to the top" do 26 | FileUtils.mkdir("tmp") unless File.exist?("tmp") 27 | @wiki.file_path = "tmp/Release-notes-br-rails.md" 28 | FileUtils.rm_f(@wiki.file_path) 29 | @wiki.add_to_top 30 | notes = IO.read(@wiki.file_path) 31 | expect(notes).to eq "#{@notes}\n" 32 | 33 | @wiki.add_to_top 34 | notes = IO.read(@wiki.file_path) 35 | expect(notes).to eq "#{@notes}\n#{@notes}\n" 36 | end 37 | 38 | it "should push" do 39 | allow(@wiki).to receive(:execute).and_return(fake_clone_dir) 40 | @wiki.push 41 | end 42 | 43 | it "should update" do 44 | allow(@wiki).to receive(:pull) 45 | allow(@wiki).to receive(:add_to_top).and_return(true) 46 | allow(@wiki).to receive(:push) 47 | @wiki.update 48 | end 49 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Running specs by default will use VCR. 2 | # Can disable via: VCR=0 rake or clean out vcr fixtures via rake clean:vcr. 3 | # Using a ENV VCR flag so that both ruby and cli specs always either use 4 | # vcr or do not use vcr. It's confusing to only have only ruby use vcr 5 | # and cli specs not using vcr and vice versa. 6 | ENV['VCR'] ? ENV['VCR'] : ENV['VCR'] = '1' 7 | ENV['TEST'] = '1' 8 | ENV['CODECLIMATE_REPO_TOKEN'] = ENV['CODECLIMATE_GANTREE_TOKEN'] 9 | 10 | require "codeclimate-test-reporter" 11 | CodeClimate::TestReporter.start 12 | 13 | require "pp" 14 | require "vcr" 15 | 16 | root = File.expand_path('../../', __FILE__) 17 | require "#{root}/lib/gantree" 18 | 19 | module Helpers 20 | def execute(cmd) 21 | puts "Running: #{cmd}" if ENV['DEBUG'] 22 | out = `#{cmd}` 23 | raise "Stack Trace Found: \n #{out}" if out.include? "Error" 24 | puts out if ENV['DEBUG'] 25 | out 26 | end 27 | 28 | def git_log_mock 29 | <<-EOL 30 | commit 1 31 | COMMIT_SEPARATOR 32 | commit 2 33 | COMMIT_SEPARATOR 34 | test up controller 35 | COMMIT_SEPARATOR 36 | commit 4 37 | COMMIT_SEPARATOR 38 | add to test 39 | note with newlie 40 | COMMIT_SEPARATOR 41 | fix bug 42 | COMMIT_SEPARATOR 43 | whatever 44 | COMMIT_SEPARATOR 45 | EOL 46 | end 47 | 48 | end 49 | 50 | RSpec.configure do |c| 51 | c.include Helpers 52 | # Use color in STDOUT 53 | c.color = true 54 | 55 | # Use color not only in STDOUT but also in pagers and files 56 | c.tty = true 57 | 58 | # Use the specified formatter 59 | c.formatter = :documentation # :progress, :html, :textmate 60 | #c.around(:each) do |example| 61 | # VCR.use_cassette(example.metadata[:full_description], :serialize_with => :json) do 62 | # example.run 63 | # end if ENV['VCR'] == '1' 64 | #end 65 | c.after(:all) do 66 | FileUtils.rm_rf("Dockerrun.aws.json") 67 | FileUtils.rm_rf("*.zip") 68 | end 69 | end 70 | # 71 | #VCR.configure do |config| 72 | # config.cassette_library_dir = "spec/fixtures/vcr" 73 | # config.hook_into :fakeweb 74 | # config.default_cassette_options = {:record => :once} 75 | # config.ignore_hosts "codeclimate.com" 76 | #end 77 | # 78 | # 79 | 80 | class Existence 81 | def self.exists? 82 | true 83 | end 84 | end 85 | 86 | class NonExistence 87 | def self.exists? 88 | false 89 | end 90 | end 91 | 92 | 93 | --------------------------------------------------------------------------------