shipment-deploy 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 86469817a1abdb8afef74ba478c6fbcc4cd09e6d
4
+ data.tar.gz: c1d42b3aa527d742484ca4a0584ce949fdf67a03
5
+ SHA512:
6
+ metadata.gz: 26e85ca97548fb373f653d9e4316cd48a89661b7046637410b6ae3f452892b28d7fb35e0db99604e240f9db6d12c0766ae27241f5a4ec7ce553e18d34186b4cf
7
+ data.tar.gz: f326df3df0e58440109d16598e947469a2d92e2ebc16fbe3ddc9d0668c0293bd3db5dc2c25663e8fbf686e369753d98fba5b55effa548c6a73bbf7b60781e32c
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --require spec_helper
3
+ --format documentation
4
+
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in shipment.gemspec
4
+ gemspec
5
+
6
+ gem 'sidekiq'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Logan Hasson
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.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # Shipment
2
+
3
+ Easy deployment using Docker
4
+
5
+ ## Installation
6
+
7
+ $ gem install shipment
8
+
9
+ ## Usage
10
+
11
+ $ ship lash # Setup credentials
12
+
13
+ $ ship this # Prepare application for deployment
14
+
15
+ $ ship out # Deploy application
16
+
17
+ ## What It Does
18
+
19
+ 1. Sets up a droplet on DigitalOcean with the proper SSH key
20
+ 2. Creates two Docker containers on the droplet: one for your
21
+ application, and one for a Postgres database
22
+ 3. Sets up a deploy key for your application's repo on GitHub
23
+ 4. Adds a `.shipment` file to your project containing
24
+ application-specific information
25
+ 5. Adds an entry to a global `.shipment` file in your home directory to
26
+ keep track of all deployments
27
+
28
+ ## Notes
29
+
30
+ 1. Setup takes between 5 and 10 minutes, depending upon the speed of
31
+ DigitalOcean
32
+ 2. During setup, green arrows indicate local ouptut, blue arrows
33
+ indicate commands run on the server, red arrows indicate failed
34
+ commands and/or connection timeouts, and unmarked output represents
35
+ stdout on the server.
36
+ 3. Some output is printed in red, even though there is no error. Some
37
+ server output is being improperly redirected to stderr.
38
+ 4. Currently, the only option is to set up a 2gb droplet in the NYC2
39
+ region.
40
+ 5. Currently, the server runs on port 3000.
41
+
42
+ ## Todo
43
+
44
+ 1. Only add `.shipmet` to `.gitignore` once, duh!
45
+ 2. Handle SSH authorization issues when re-setting up. DigitalOcean
46
+ allows adding multiple keys with the same name. Need to delete
47
+ existing key locally and remotely when running setup again. (Probably
48
+ should either remove `ship setup` command or do some checking when that
49
+ command is run.)
50
+ 3. Need to deal with commit limit. Containers need to be exported every
51
+ once in a while to clean up Aufs layers.
52
+ 4. Save and push application-specific docker images to user's docker
53
+ account.
54
+ 5. Allow deploying from branches other than master.
55
+ 6. Error handle *way* better. Need to be able to handle partial
56
+ setups/deploys.
57
+ 7. Look into creating a base image that already includes ruby and
58
+ postgres containers. (Requires support from DigitalOcean.)
59
+ 8. Look into using `docker attach` to allow viewing logs.
60
+ 9. Better deal with committing and pushing on `ship this`. Right now,
61
+ rejected pushes would break everything.
62
+ 10. Create simple way to symlink credentials from an `application.yml` or
63
+ `.env` file.
64
+ 11. Fix messy output during setup and deploy. Also, deal with duplicate
65
+ 'Done.'s when a command is re-run.
66
+ 12. Speed everything up.
67
+ 13. Clean up the classes. Most things are doing *way* too much.
68
+ 14. Write specs!
69
+ 15. Add cleanup option to remove all system config.
70
+
71
+ ## Contributing
72
+
73
+ 1. Fork it ( https://github.com/loganhasson/shipment/fork )
74
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
75
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
76
+ 4. Push to the branch (`git push origin my-new-feature`)
77
+ 5. Create a new Pull Request
78
+
79
+ # Author
80
+
81
+ [@loganhasson](http://twitter.com/loganhasson)
82
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/ship ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'shipment/cli'
4
+
5
+ if ARGV[0] == '.'
6
+ ARGV[0] = "this"
7
+ end
8
+
9
+ Shipment::CLI.start(ARGV)
data/lib/shipment.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "shipment/version"
2
+
3
+ module Shipment
4
+ # Your code goes here...
5
+ end
6
+
@@ -0,0 +1,56 @@
1
+ require 'thor'
2
+ require 'shipment/mooring'
3
+ require 'shipment/credentials_checker'
4
+ require 'shipment/rigging'
5
+ require 'shipment/slip'
6
+
7
+ module Shipment
8
+
9
+ class CLI < Thor
10
+ desc "lash", "Setup shipment with your GitHub and DigitalOcean credentials"
11
+ long_desc <<-LONGDESC
12
+ `ship lash` will collect your GitHub and DigitalOcean credentials.
13
+ LONGDESC
14
+ def lash
15
+ Shipment::Mooring.lash
16
+ end
17
+
18
+ desc "this", "Prepare your application for deployment"
19
+ long_desc <<-LONGDESC
20
+ `ship this` will setup all necessary files and prepare a remote server for
21
+ deployment.
22
+
23
+ This process includes adding the `mina` gem to your Gemfile, creating a new
24
+ `deploy.rb` file, altering your `database.yml` file for use on the remote
25
+ server, and ensuring that all necessary SSH keys are in place.
26
+
27
+ Alternate usage: `ship .`
28
+ LONGDESC
29
+ def this
30
+ if !Shipment::CredentialsChecker.verify
31
+ Shipment::Mooring.lash
32
+ end
33
+
34
+ Shipment::Rigging.rig
35
+ end
36
+
37
+ desc "out", "Deploy your application"
38
+ long_desc <<-LONGDESC
39
+ `ship out` will deploy your application's master branch to your DigitalOcean
40
+ server.
41
+ LONGDESC
42
+ def out
43
+ if !Shipment::CredentialsChecker.verify
44
+ Shipment::Mooring.lash
45
+ end
46
+
47
+ if !File.exist?('.shipment')
48
+ Shipment::Rigging.rig
49
+ end
50
+
51
+ Shipment::Slip.cast_off
52
+ end
53
+ end
54
+
55
+ end
56
+
@@ -0,0 +1,23 @@
1
+ require 'netrc'
2
+
3
+ module Shipment
4
+
5
+ class CredentialsChecker
6
+ attr_reader :netrc
7
+
8
+ def self.verify
9
+ new.verify
10
+ end
11
+
12
+ def initialize
13
+ @netrc = Netrc.read
14
+ end
15
+
16
+ def verify
17
+ ![netrc["shipment.gh"], netrc["shipment.do"]].flatten.any? do |cred|
18
+ cred.nil? || cred.empty? || cred == " "
19
+ end
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,156 @@
1
+ require 'netrc'
2
+ require 'sshkey'
3
+ require 'octokit'
4
+ require 'digitalocean'
5
+ require 'highline/import'
6
+
7
+ module Shipment
8
+
9
+ class Mooring
10
+ attr_accessor :gh_username, :gh_password, :gh_token,
11
+ :do_client_id, :do_api_key,
12
+ :ssh_key, :private_key, :public_key
13
+ def self.lash
14
+ new.lash
15
+ end
16
+
17
+ def lash
18
+ write_gh_creds
19
+ write_do_creds
20
+ do_ssh_success = setup_do_ssh_key
21
+ while !do_ssh_success
22
+ puts "Your DigitalOcean credentials are invalid. Please try again."
23
+ write_do_creds
24
+ do_ssh_success = setup_do_ssh_key
25
+ end
26
+ end
27
+
28
+ def write_gh_creds
29
+ collect_gh_creds
30
+ write_creds(:gh)
31
+ end
32
+
33
+ def collect_gh_creds
34
+ self.gh_username = ask('GitHub Username: ')
35
+ self.gh_password = ask('GitHub Password (Never Stored): ') do |q|
36
+ q.echo = false
37
+ end
38
+ self.gh_token = get_gh_token
39
+ end
40
+
41
+ def get_gh_token
42
+ success = true
43
+ token_exists = false
44
+
45
+ client = Octokit::Client.new(login: gh_username, password: gh_password)
46
+
47
+ begin
48
+ authorization = client.create_authorization(
49
+ scopes: [
50
+ "user",
51
+ "repo",
52
+ "public_repo",
53
+ "write:public_key",
54
+ "read:public_key",
55
+ "read:org",
56
+ "read:repo_hook",
57
+ "write:repo_hook",
58
+ "repo_deployment",
59
+ "admin:public_key",
60
+ "admin:repo_hook"
61
+ ],
62
+ note: "Shipment Token"
63
+ )
64
+ rescue Octokit::UnprocessableEntity => e
65
+ success = false
66
+ if !!e.message.match(/already_exists/)
67
+ token_exists = true
68
+
69
+ puts <<-MSG.gsub(/^ {12}/,'')
70
+ A token for Shipment already exists for your GitHub account. This
71
+ may or may not cause issues. Please visit your GitHub account,
72
+ delete the existing token, and try this setup again.
73
+ MSG
74
+ end
75
+ rescue Octokit::Unauthorized
76
+ success = false
77
+ puts "Your GitHub username and password are invalid. Please try again."
78
+ end
79
+
80
+ if success
81
+ return authorization[:token]
82
+ elsif token_exists
83
+ exit
84
+ else
85
+ collect_gh_creds
86
+ end
87
+ end
88
+
89
+ def write_do_creds
90
+ collect_do_creds
91
+ write_creds(:do)
92
+ end
93
+
94
+ def collect_do_creds
95
+ self.do_client_id = ask("DigitalOcean Client ID: ")
96
+ self.do_api_key = ask("DigitalOcean API Key: ")
97
+ end
98
+
99
+ def write_creds(provider)
100
+ netrc = Netrc.read
101
+
102
+ if provider == :do
103
+ netrc["shipment.do"] = do_client_id, do_api_key
104
+ elsif provider == :gh
105
+ netrc["shipment.gh"] = gh_username, gh_token
106
+ end
107
+
108
+ netrc.save
109
+ end
110
+
111
+ def setup_do_ssh_key
112
+ create_key
113
+ transmit_do_key
114
+ end
115
+
116
+ def create_key
117
+ self.ssh_key = SSHKey.generate
118
+ self.private_key = write_private_key
119
+ self.public_key = write_public_key
120
+ end
121
+
122
+ def write_private_key
123
+ File.open("#{ENV['HOME']}/.ssh/shipment_rsa", "w+") do |f|
124
+ f.write ssh_key.private_key
125
+ end
126
+
127
+ `chmod 0600 #{ENV['HOME']}/.ssh/shipment_rsa`
128
+ return ssh_key.private_key
129
+ end
130
+
131
+ def write_public_key
132
+ File.open("#{ENV['HOME']}/.ssh/shipment_rsa.pub", "w+") do |f|
133
+ f.write ssh_key.ssh_public_key
134
+ end
135
+
136
+ return ssh_key.ssh_public_key
137
+ end
138
+
139
+ def transmit_do_key
140
+ Digitalocean.client_id = do_client_id
141
+ Digitalocean.api_key = do_api_key
142
+
143
+ response = Digitalocean::SshKey.create({
144
+ name: 'shipment',
145
+ ssh_pub_key: CGI::escape(public_key)
146
+ })
147
+
148
+ if response.status == "ERROR"
149
+ return false
150
+ else
151
+ return true
152
+ end
153
+ end
154
+ end
155
+
156
+ end
@@ -0,0 +1,99 @@
1
+ require 'git'
2
+ require 'securerandom'
3
+ require 'shipment/server/ssh_client'
4
+
5
+ module Shipment
6
+ module Project
7
+
8
+ class Customizer
9
+ attr_accessor :repo_info
10
+
11
+ def self.customize
12
+ puts "-----> ".green + "Setting up deployment settings..."
13
+ new.customize
14
+ end
15
+
16
+ def initialize
17
+ @repo_info = YAML.load(File.read('.shipment'))
18
+ end
19
+
20
+ def customize
21
+ puts "-----> ".green + "Checking for Redis and Sidqkiq..."
22
+ if has_sidekiq?
23
+ add_sidekiq
24
+ elsif has_redis?
25
+ add_redis
26
+ end
27
+
28
+ puts "-----> ".green + "Updating database info..."
29
+ update_db_info
30
+ puts "-----> ".green + "Adding .shipment file to .gitignore..."
31
+ add_shipment_to_gitignore
32
+ puts "-----> ".green + "Committing and pushing changes..."
33
+ commit_and_push
34
+ end
35
+
36
+ def commit_and_push
37
+ git = Git.open(FileUtils.pwd)
38
+ git.add(all: true)
39
+ git.commit("Setup for deployment with shipment")
40
+ git.push(git.remote("origin"))
41
+ end
42
+
43
+ def add_shipment_to_gitignore
44
+ File.open(".gitignore", "a") do |f|
45
+ f.write '.shipment'
46
+ end
47
+ end
48
+
49
+ def has_sidekiq?
50
+ !!File.read('Gemfile').match(/sidekiq/)
51
+ end
52
+
53
+ def add_sidekiq
54
+ repo_info[:sidekiq] = true
55
+ add_redis
56
+ end
57
+
58
+ def has_redis?
59
+ !!File.read('Gemfile').match(/redis/)
60
+ end
61
+
62
+ def add_redis
63
+ repo_info[:redis] = true
64
+ end
65
+
66
+ def update_shipment_file
67
+ File.open('.shipment', 'w+') do |f|
68
+ f.write repo_info.to_yaml
69
+ end
70
+ end
71
+
72
+ def update_db_info
73
+ create_db_creds
74
+ update_shipment_file
75
+ update_db_yaml
76
+ end
77
+
78
+ def update_db_yaml
79
+ FileUtils.cp('config/database.yml', 'config/database.yml.bak')
80
+ database_yml = YAML.load(File.read('config/database.yml'))
81
+ database_yml["production"] = {
82
+ "url" => "postgres://#{repo_info[:database][:username]}:#{repo_info[:database][:password]}@#{repo_info[:ip_address]}:5432/#{repo_info[:database][:name]}"
83
+ }
84
+ File.open("config/database.yml", "w+") do |f|
85
+ f.write database_yml.to_yaml
86
+ end
87
+ end
88
+
89
+ def create_db_creds
90
+ repo_info[:database] = {
91
+ username: SecureRandom.hex(5).chars.unshift('s').join,
92
+ password: SecureRandom.hex.chars.unshift('s').join,
93
+ name: SecureRandom.hex(10).chars.unshift('s').join
94
+ }
95
+ end
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,39 @@
1
+ require 'git'
2
+ require 'netrc'
3
+ require 'yaml'
4
+
5
+ module Shipment
6
+ module Project
7
+
8
+ class Repo
9
+ attr_accessor :repo, :url, :user, :name, :original_name
10
+
11
+ def initialize
12
+ self.repo = Git.open(FileUtils.pwd)
13
+ parse_details
14
+ end
15
+
16
+ def parse_details
17
+ get_url
18
+ get_user
19
+ get_name
20
+ end
21
+
22
+ def get_url
23
+ self.url = repo.remotes.detect do |remote|
24
+ remote.name == "origin"
25
+ end.url
26
+ end
27
+
28
+ def get_user
29
+ self.user = url.match(/(?:https:\/\/|git@)github\.com(?:\:|\/)(.*)\/.+(?:\.git)?/)[1]
30
+ end
31
+
32
+ def get_name
33
+ self.original_name = url.match(/(?:https:\/\/|git@).*\/(.+)(?:\.git)?/)[1].gsub('.git', '')
34
+ self.name = url.match(/(?:https:\/\/|git@).*\/(.+)(?:\.git)?/)[1].gsub('.git', '').gsub('_','-')
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,16 @@
1
+ require 'shipment/server/initializer'
2
+ require 'shipment/project/repo'
3
+
4
+ module Shipment
5
+
6
+ class Rigging
7
+ def self.rig
8
+ new.rig
9
+ end
10
+
11
+ def rig
12
+ Shipment::Server::Initializer.spin_up(Shipment::Project::Repo.new)
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,121 @@
1
+ require 'digitalocean'
2
+ require 'netrc'
3
+ require 'colorize'
4
+ require 'shipment/server/ssh_client'
5
+ require 'shipment/project/customizer'
6
+ require 'yaml'
7
+
8
+ module Shipment
9
+ module Server
10
+
11
+ class Initializer
12
+ attr_reader :repo_url, :repo_name, :repo_user, :repo
13
+ attr_accessor :droplet, :ip_address
14
+
15
+ def self.spin_up(repo)
16
+ new(repo).spin_up
17
+ end
18
+
19
+ def initialize(repo)
20
+ @repo = repo
21
+ @repo_url, @repo_name, @repo_user = repo.url, repo.name, repo.user
22
+ netrc = Netrc.read
23
+ Digitalocean.client_id, Digitalocean.api_key = netrc["shipment.do"]
24
+ end
25
+
26
+ def spin_up
27
+ create_droplet
28
+ store_droplet_data
29
+ update_ssh_config
30
+ Shipment::Project::Customizer.customize
31
+ Shipment::Server::SSHClient.setup(
32
+ repo: repo,
33
+ ip_address: ip_address
34
+ )
35
+ end
36
+
37
+ def create_droplet
38
+ print "-----> ".green + "Creating Droplet: #{repo_name}"
39
+ self.droplet = Digitalocean::Droplet.create({
40
+ name: repo_name,
41
+ size_id: get_size_id,
42
+ image_id: get_image_id,
43
+ region_id: get_region_id,
44
+ ssh_key_ids: [get_ssh_key_id]
45
+ }).droplet
46
+ print_waiting
47
+ end
48
+
49
+ def store_droplet_data
50
+ puts "-----> ".green + "Storing droplet data..."
51
+ File.open("#{ENV['HOME']}/.shipment", "a") do |f|
52
+ f.write "#{droplet.id} : #{repo_name} : #{ip_address}\n"
53
+ end
54
+
55
+ puts "-----> ".green + "Creating .shipment file..."
56
+ yaml = {
57
+ id: droplet.id,
58
+ ip_address: ip_address,
59
+ name: repo_name,
60
+ user: repo_user,
61
+ url: repo_url,
62
+ secret: `rake secret`.strip
63
+ }.to_yaml
64
+ File.open(File.join(FileUtils.pwd, ".shipment"), "w+") do |f|
65
+ f.write yaml
66
+ end
67
+ end
68
+
69
+ def update_ssh_config
70
+ puts "-----> ".green + "Setting up SSH config..."
71
+ File.open("#{ENV['HOME']}/.ssh/config", "a") do |f|
72
+ f.write <<-SSHCONFIG.gsub(/^ {10}/,'')
73
+
74
+ Host #{ip_address}
75
+ Hostname #{ip_address}
76
+ IdentityFile #{ENV['HOME']}/.ssh/shipment_rsa
77
+ User root
78
+ SSHCONFIG
79
+ end
80
+ end
81
+
82
+ def get_size_id
83
+ Digitalocean::Size.all.sizes.detect {|size| size.slug == "2gb"}.id
84
+ end
85
+
86
+ def get_image_id
87
+ Digitalocean::Image.all.images.detect {|image| image.name.match(/Docker/)}.id
88
+ end
89
+
90
+ def get_region_id
91
+ Digitalocean::Region.all.regions.detect {|region| region.slug == "nyc2"}.id
92
+ end
93
+
94
+ def get_ssh_key_id
95
+ Digitalocean::SshKey.all.ssh_keys.detect {|key| key.name == "shipment"}.id
96
+ end
97
+
98
+ def droplet_created?
99
+ !!(remote_droplet.status == "active")
100
+ end
101
+
102
+ def get_ip_address
103
+ remote_droplet.ip_address
104
+ end
105
+
106
+ def remote_droplet
107
+ Digitalocean::Droplet.find(droplet.id).droplet
108
+ end
109
+
110
+ def print_waiting
111
+ while !droplet_created?
112
+ print "."
113
+ sleep 3
114
+ end
115
+
116
+ self.ip_address = get_ip_address
117
+ puts "Done."
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,218 @@
1
+ require 'net/ssh'
2
+ require 'netrc'
3
+ require 'highline/import'
4
+ require 'colorize'
5
+
6
+ module Shipment
7
+ module Server
8
+
9
+ class SSHClient
10
+ attr_reader :ip_address, :repo, :repo_url, :repo_name, :repo_user,
11
+ :repo_original_name, :gh_username, :gh_token, :db_user,
12
+ :db_password, :db_name, :secret_key_base
13
+
14
+ def self.setup(repo:, ip_address:)
15
+ new(repo, ip_address).setup
16
+ end
17
+
18
+ def self.deploy(repo:, ip_address:)
19
+ new(repo, ip_address).deploy
20
+ end
21
+
22
+ def initialize(repo, ip_address)
23
+ @gh_username, @gh_token = Netrc.read["shipment.gh"]
24
+ @ip_address = ip_address
25
+ @repo_url, @repo_name, @repo_user, @repo_original_name = repo.url, repo.name, repo.user, repo.original_name
26
+ db_info = YAML.load(File.read('.shipment'))[:database]
27
+ @secret_key_base = YAML.load(File.read('.shipment'))[:secret]
28
+ @db_user, @db_password, @db_name = db_info[:username], db_info[:password], db_info[:name]
29
+ end
30
+
31
+ def setup
32
+ add_to_known_hosts
33
+ puts "-----> ".green + "Preparing server..."
34
+ setup_database_container
35
+ setup_application_container
36
+ end
37
+
38
+ def deploy
39
+ puts "-----> ".green + "Deploying..."
40
+ kill_and_commit_old_server
41
+ start_new_server
42
+ puts "-----> ".green + "Done.\nYour application is accessable at: #{ip_address}"
43
+
44
+ end
45
+
46
+ def kill_and_commit_old_server
47
+ puts "-----> ".green + "Stopping existing server..."
48
+ run_remote_command("if sudo docker ps -a | grep application > /dev/null; then docker kill application && docker commit application #{repo_user}/#{repo_name} && docker rm application; fi")
49
+ end
50
+
51
+ def start_new_server
52
+ puts "-----> ".green + "Restarting server..."
53
+ redis_command, sidekiq_command = parse_redis_and_sidekiq
54
+ run_remote_command("docker run -d -p 80:3000 --name application -e SECRET_KEY_BASE=#{secret_key_base} #{repo_user}/#{repo_name} /bin/bash -c 'kill -9 $(pgrep -f sidekiq) > /dev/null 2>&1 && kill -9 $(pgrep -f redis-server) > /dev/null 2>&1 && kill -9 $(pgrep -f rails) > /dev/null 2>&1 && source /etc/profile.d/rvm.sh && rm -rf #{repo_name} && git clone #{repo_url} #{repo_name} && cd #{repo_name} && bundle install && RAILS_ENV=production bundle exec rake db:migrate#{redis_command}#{sidekiq_command} && rails server -p 3000 -e production'")
55
+ end
56
+
57
+ def parse_redis_and_sidekiq
58
+ # Code smell...
59
+ commands = []
60
+ if YAML.load(File.read('.shipment'))[:sidekiq]
61
+ commands << ' && redis-server /etc/redis/redis.conf'
62
+ commands << ' && bundle exec sidekiq -d -L log/sidekiq.log'
63
+ elsif YAML.load(File.read('.shipment'))[:redis]
64
+ commands << ' && redis-server /etc/redis/redis.conf'
65
+ end
66
+
67
+ return commands
68
+ end
69
+
70
+ def add_to_known_hosts
71
+ puts "-----> ".green + "Giving server time to prepare for SSH connections (this will take about 1 minute)..."
72
+ sleep 60
73
+ puts "-----> ".green + "Adding droplet to known hosts..."
74
+ FileUtils.touch('known_host.sh')
75
+ FileUtils.chmod('+x', 'known_host.sh')
76
+ File.open('known_host.sh', 'w+') do |f|
77
+ f.write <<-SCRIPT.gsub(/^ {10}/, '')
78
+ #!/bin/bash
79
+
80
+ /usr/bin/expect <<EOD
81
+ spawn ssh root@#{ip_address}
82
+ expect -re "(continue)"
83
+ send "yes\\n"
84
+ send "exit\\n"
85
+ expect eof
86
+ EOD
87
+ SCRIPT
88
+ end
89
+ `./known_host.sh && rm known_host.sh`
90
+ end
91
+
92
+ def setup_database_container
93
+ puts "-----> ".green + "Setting up database container..."
94
+ pull_postgres_image
95
+ setup_database_user
96
+ start_database_container
97
+ end
98
+
99
+ def pull_postgres_image
100
+ puts "-----> ".green + "Pulling Postgres image (this may take a few minutes)..."
101
+ run_remote_command("docker pull loganhasson/postgres_image")
102
+ end
103
+
104
+ def setup_database_user
105
+ puts "-----> ".green + "Setting up database user..."
106
+ run_remote_command("docker run --name database -e USER=#{db_user} -e PASSWORD=#{db_password} -e NAME=#{db_name} loganhasson/postgres_image /bin/bash -c 'wget https://gist.githubusercontent.com/loganhasson/d8f8a91875087407ea6a/raw/21bdbd6134e470397cef1ab0feeefdecee3fb6f0/database_setup_shipment.sql && service postgresql start && psql --set \"user=$USER\" --set \"password=$PASSWORD\" --file=database_setup_shipment.sql && createdb -O $USER $NAME && service postgresql stop && rm database_setup_shipment.sql && wget https://gist.githubusercontent.com/loganhasson/680f4b02c0f295dd7961/raw/18af1b0ccb2bb485dfc51e2fde169fce91393bca/update_postgres_config_shipment.sh && chmod +x update_postgres_config_shipment.sh && ./update_postgres_config_shipment.sh && rm update_postgres_config_shipment.sh' && docker commit database #{repo_user}/#{repo_name}_db && docker rm database && docker rmi loganhasson/postgres_image")
107
+ end
108
+
109
+ def start_database_container
110
+ puts "-----> ".green + "Starting postgres server..."
111
+ run_remote_command("docker run --name database -d -p 5432:5432 #{repo_user}/#{repo_name}_db su postgres -c '/usr/lib/postgresql/9.3/bin/postgres -D /var/lib/postgresql/9.3/main -c config_file=/etc/postgresql/9.3/main/postgresql.conf'")
112
+ end
113
+
114
+ def setup_application_container
115
+ puts "-----> ".green + "Setting up application container..."
116
+ pull_ruby_image
117
+ generate_ssh_key
118
+ add_deploy_key
119
+ clone_and_bundle
120
+ migrate
121
+ #save_docker_image
122
+
123
+ puts "-----> ".green + "Done.\nReady to deploy."
124
+ puts "Your server's IP Address is: #{ip_address}"
125
+ end
126
+
127
+ def pull_ruby_image
128
+ puts "-----> ".green + "Pulling Ruby image (this may take a few minutes)..."
129
+ run_remote_command("docker pull loganhasson/ruby_image")
130
+ end
131
+
132
+ def generate_ssh_key
133
+ puts "-----> ".green + "Generating deploy key..."
134
+ run_remote_command("docker run --name setup loganhasson/ruby_image /bin/bash -c 'ssh-keygen -t rsa -N \"\" -C \"#{repo_name}@shipment\" -f \"/root/.ssh/id_rsa\"' && docker commit setup #{repo_user}/#{repo_name} && docker rm setup && docker rmi loganhasson/ruby_image")
135
+ end
136
+
137
+ def add_deploy_key
138
+ puts "-----> ".green + "Adding deploy key to GitHub..."
139
+ run_remote_command("docker run --name deploy -e ACCESS_TOKEN=#{gh_token} -e REPO=#{repo_user}/#{repo_original_name} #{repo_user}/#{repo_name} /bin/bash -c 'source /etc/profile.d/rvm.sh && wget https://gist.githubusercontent.com/loganhasson/e4791b4abe2dc75bc82f/raw/342cd7c32cec4e58947c0eb1785f45abecf714c4/add_deploy_key_shipment.rb && ruby add_deploy_key_shipment.rb && rm add_deploy_key_shipment.rb' && docker commit deploy #{repo_user}/#{repo_name} && docker rm deploy")
140
+ end
141
+
142
+ def clone_and_bundle
143
+ puts "-----> ".green + "Cloning project into application container..."
144
+ run_remote_command("docker run --name bundle -e REPO_URL=#{repo_url} -e REPO_NAME=#{repo_name} #{repo_user}/#{repo_name} /bin/bash -c 'source /etc/profile.d/rvm.sh && wget https://gist.githubusercontent.com/loganhasson/5db07d7b5671a9bc5fef/raw/4a28de693b2631c69a888fada82d6b462e2fe09e/clone_with_expect_shipment.sh && chmod +x clone_with_expect_shipment.sh && ./clone_with_expect_shipment.sh && rm clone_with_expect_shipment.sh && cd #{repo_name} && bundle install && cd ..' && docker commit bundle #{repo_user}/#{repo_name} && docker rm bundle")
145
+ end
146
+
147
+ def migrate
148
+ puts "-----> ".green + "Migrating database..."
149
+ run_remote_command("docker run --name migrate #{repo_user}/#{repo_name} /bin/bash -c 'source /etc/profile.d/rvm.sh && cd #{repo_name} && RAILS_ENV=production bundle exec rake db:migrate' && docker rm migrate")
150
+ end
151
+
152
+ #def save_docker_image
153
+ #puts "-----> ".green + "Installing required packages (this may take a few minutes)..."
154
+ #run_remote_command("apt-get -y update && apt-get -y upgrade && apt-get -y udpate && apt-get -y install expect", true)
155
+
156
+ #puts "-----> ".green + "Saving docker images and pushing to Docker hub..."
157
+ #docker_username = ask("Docker username: ")
158
+ #docker_password = ask("Docker password: ") { |q| q.echo = false }
159
+ #docker_email = ask("Docker email address: ")
160
+
161
+ #run_remote_command(<<-SETUP
162
+ #/usr/bin/expect <<EOD
163
+ #spawn docker login
164
+ #expect "Username:"
165
+ #send "#{docker_username}\n"
166
+ #expect "Password:"
167
+ #send "#{docker_password}\n"
168
+ #expect "Email:"
169
+ #send "#{docker_email}\n"
170
+ #expect eof
171
+ #EOD
172
+ #SETUP
173
+ #)
174
+ #run_remote_command("docker push #{repo_user}/#{repo_name} && docker push #{repo_user}/#{repo_name}_db")
175
+ #end
176
+
177
+ def run_remote_command(command, silent=false)
178
+ puts "-----> ".blue + "#{command}"
179
+
180
+ begin
181
+ Net::SSH.start(ip_address, 'root', timeout: 500) do |ssh|
182
+ ssh.open_channel do |channel|
183
+ channel.exec "#{command}" do |ch, success|
184
+ raise "problem executing command: #{command}".red unless success
185
+
186
+ ch.on_data do |c, data|
187
+ if !silent
188
+ if !data.empty? && !(data == " ") && !(data == "\n") && !data.match(/ojbects|deltas/) && !(data == ".")
189
+ $stdout.puts " #{data.strip.chomp}"
190
+ end
191
+ end
192
+ end
193
+
194
+ ch.on_extended_data do |c, type, data|
195
+ if !data.empty? && !(data == " ") && !(data == "\n") && !(data == ".")
196
+ $stderr.puts " #{data.strip.chomp}".red
197
+
198
+ if data.strip.chomp.match(/Error pulling image/) || data.strip.chomp.match(/connection timed out/)
199
+ $stderr.puts "-----> ".red + "CONNECTION TO DOCKER TIMED OUT: ".red + "Trying again..."
200
+ run_remote_command(command, silent)
201
+ end
202
+ end
203
+ end
204
+
205
+ #ch.on_close { puts "-----> ".green + "Done." }
206
+ end
207
+ end
208
+ end
209
+ rescue Errno::ETIMEDOUT
210
+ $stderr.puts "-----> ".red + "CONNECTION TO SERVER TIMED OUT: ".red + "Reconnecting..."
211
+ run_remote_command(command, silent)
212
+ end
213
+ end
214
+ end
215
+
216
+ end
217
+ end
218
+
@@ -0,0 +1,18 @@
1
+ require 'yaml'
2
+ require 'shipment/server/ssh_client'
3
+ require 'shipment/project/repo'
4
+
5
+ module Shipment
6
+ class Slip
7
+ def self.cast_off
8
+ new.cast_off
9
+ end
10
+
11
+ def cast_off
12
+ Shipment::Server::SSHClient.deploy(
13
+ repo: Shipment::Project::Repo.new,
14
+ ip_address: YAML.load(File.read('.shipment'))[:ip_address]
15
+ )
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,4 @@
1
+ module Shipment
2
+ VERSION = "0.0.3"
3
+ end
4
+
data/shipment.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'shipment/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "shipment-deploy"
8
+ spec.version = Shipment::VERSION
9
+ spec.authors = ["Logan Hasson"]
10
+ spec.email = ["logan.hasson@gmail.com"]
11
+ spec.summary = "Easy deployment using Docker"
12
+ spec.description = "Automatically deploy Rails apps to DigitalOcean in a \
13
+ Docker container."
14
+ spec.homepage = "http://github.com/loganhasson/shipment"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib", "bin"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.6"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "spec"
25
+
26
+ spec.add_runtime_dependency "net-ssh"
27
+ spec.add_runtime_dependency "digitalocean"
28
+ spec.add_runtime_dependency "octokit"
29
+ spec.add_runtime_dependency "netrc"
30
+ spec.add_runtime_dependency "thor"
31
+ spec.add_runtime_dependency "highline"
32
+ spec.add_runtime_dependency "sshkey"
33
+ spec.add_runtime_dependency "git"
34
+ spec.add_runtime_dependency "colorize"
35
+ end
36
+
@@ -0,0 +1,18 @@
1
+ require_relative '../config/environment'
2
+
3
+ RSpec.configure do |config|
4
+ config.filter_run :focus
5
+ config.run_all_when_everything_filtered = true
6
+
7
+ config.order = :random
8
+
9
+ config.expect_with :rspec do |expectations|
10
+ expectations.syntax = :expect
11
+ end
12
+
13
+ config.mock_with :rspec do |mocks|
14
+ mocks.syntax = :expect
15
+ mocks.verify_partial_doubles = true
16
+ end
17
+ end
18
+
metadata ADDED
@@ -0,0 +1,236 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shipment-deploy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Logan Hasson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: spec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: net-ssh
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: digitalocean
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: octokit
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: netrc
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: thor
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: highline
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: sshkey
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: git
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: colorize
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description: Automatically deploy Rails apps to DigitalOcean in a Docker
182
+ container.
183
+ email:
184
+ - logan.hasson@gmail.com
185
+ executables:
186
+ - ship
187
+ extensions: []
188
+ extra_rdoc_files: []
189
+ files:
190
+ - ".gitignore"
191
+ - ".rspec"
192
+ - Gemfile
193
+ - LICENSE.txt
194
+ - README.md
195
+ - Rakefile
196
+ - bin/ship
197
+ - lib/shipment.rb
198
+ - lib/shipment/cli.rb
199
+ - lib/shipment/credentials_checker.rb
200
+ - lib/shipment/mooring.rb
201
+ - lib/shipment/project/customizer.rb
202
+ - lib/shipment/project/repo.rb
203
+ - lib/shipment/rigging.rb
204
+ - lib/shipment/server/initializer.rb
205
+ - lib/shipment/server/ssh_client.rb
206
+ - lib/shipment/slip.rb
207
+ - lib/shipment/version.rb
208
+ - shipment.gemspec
209
+ - spec/spec_helper.rb
210
+ homepage: http://github.com/loganhasson/shipment
211
+ licenses:
212
+ - MIT
213
+ metadata: {}
214
+ post_install_message:
215
+ rdoc_options: []
216
+ require_paths:
217
+ - lib
218
+ - bin
219
+ required_ruby_version: !ruby/object:Gem::Requirement
220
+ requirements:
221
+ - - ">="
222
+ - !ruby/object:Gem::Version
223
+ version: '0'
224
+ required_rubygems_version: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ">="
227
+ - !ruby/object:Gem::Version
228
+ version: '0'
229
+ requirements: []
230
+ rubyforge_project:
231
+ rubygems_version: 2.2.2
232
+ signing_key:
233
+ specification_version: 4
234
+ summary: Easy deployment using Docker
235
+ test_files:
236
+ - spec/spec_helper.rb