shipment-deploy 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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