shuttle-deploy 0.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +25 -0
  3. data/.magnum.yml +1 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE +18 -0
  7. data/README.md +230 -0
  8. data/Rakefile +10 -0
  9. data/bin/shuttle +60 -0
  10. data/examples/rails.yml +22 -0
  11. data/examples/static.yml +10 -0
  12. data/examples/wordpress.yml +34 -0
  13. data/lib/shuttle/config.rb +5 -0
  14. data/lib/shuttle/deploy.rb +58 -0
  15. data/lib/shuttle/deployment/nodejs.rb +48 -0
  16. data/lib/shuttle/deployment/php.rb +17 -0
  17. data/lib/shuttle/deployment/rails.rb +116 -0
  18. data/lib/shuttle/deployment/ruby.rb +40 -0
  19. data/lib/shuttle/deployment/static.rb +5 -0
  20. data/lib/shuttle/deployment/wordpress/cli.rb +27 -0
  21. data/lib/shuttle/deployment/wordpress/core.rb +50 -0
  22. data/lib/shuttle/deployment/wordpress/plugins.rb +112 -0
  23. data/lib/shuttle/deployment/wordpress/vip.rb +84 -0
  24. data/lib/shuttle/deployment/wordpress.rb +191 -0
  25. data/lib/shuttle/errors.rb +5 -0
  26. data/lib/shuttle/helpers.rb +50 -0
  27. data/lib/shuttle/runner.rb +153 -0
  28. data/lib/shuttle/session.rb +52 -0
  29. data/lib/shuttle/support/bundler.rb +45 -0
  30. data/lib/shuttle/support/foreman.rb +7 -0
  31. data/lib/shuttle/support/thin.rb +59 -0
  32. data/lib/shuttle/target.rb +23 -0
  33. data/lib/shuttle/tasks.rb +264 -0
  34. data/lib/shuttle/version.rb +3 -0
  35. data/lib/shuttle.rb +35 -0
  36. data/shuttle-deploy.gemspec +28 -0
  37. data/spec/deploy_spec.rb +4 -0
  38. data/spec/fixtures/.gitkeep +0 -0
  39. data/spec/fixtures/static.yml +11 -0
  40. data/spec/helpers_spec.rb +42 -0
  41. data/spec/spec_helper.rb +15 -0
  42. data/spec/target_spec.rb +41 -0
  43. metadata +232 -0
@@ -0,0 +1,40 @@
1
+ module Shuttle
2
+ class Ruby < Shuttle::Deploy
3
+ include Shuttle::Support::Bundler
4
+ include Shuttle::Support::Thin
5
+
6
+ def setup
7
+ unless ruby_installed?
8
+ error "Please install Ruby first"
9
+ end
10
+
11
+ unless bundle_installed?
12
+ install_bundler
13
+ end
14
+
15
+ super
16
+ end
17
+
18
+ def deploy
19
+ setup
20
+ update_code
21
+ checkout_code
22
+ bundle_install
23
+ thin_restart
24
+ link_shared_paths
25
+ link_release
26
+ end
27
+
28
+ def link_shared_paths
29
+ ssh.run("mkdir -p #{release_path('tmp')}")
30
+ ssh.run("ln -s #{shared_path('pids')} #{release_path('tmp/pids')}")
31
+ ssh.run("ln -s #{shared_path('log')} #{release_path('log')}")
32
+ end
33
+
34
+ private
35
+
36
+ def ruby_installed?
37
+ ssh.run("which ruby").success?
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ module Shuttle
2
+ class Static < Shuttle::Deploy
3
+ # Does not implement any custom functionality
4
+ end
5
+ end
@@ -0,0 +1,27 @@
1
+ module Shuttle
2
+ module WordpressCli
3
+ CLI_GIT = 'https://github.com/wp-cli/wp-cli.git'
4
+ CLI_PATH = '/usr/local/share/wp-cli'
5
+
6
+ # Check if CLI is installed
7
+ # @return [Boolean]
8
+ def cli_installed?
9
+ ssh.run("which wp").success?
10
+ end
11
+
12
+ # Install wordpress CLI
13
+ # @return [Boolean]
14
+ def cli_install
15
+ log "Installing wordpress CLI"
16
+
17
+ ssh.run("sudo git clone --recursive --quiet #{CLI_GIT} #{CLI_PATH}")
18
+ ssh.run("cd #{CLI_PATH} && sudo utils/dev-build")
19
+
20
+ if cli_installed?
21
+ log "Wordpress CLI installed"
22
+ else
23
+ error "Unable to install wordpress CLI"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,50 @@
1
+ module Shuttle
2
+ module WordpressCore
3
+ # Get wordpress shared core path
4
+ # @return [String]
5
+ def core_path
6
+ @core_path ||= shared_path('wordpress/core')
7
+ end
8
+
9
+ # Check if wordpress core is installed
10
+ # @return [Boolean]
11
+ def core_installed?
12
+ ssh.directory_exists?(core_path) &&
13
+ !ssh.capture("ls #{core_path}").empty?
14
+ end
15
+
16
+ # Install wordpress shared core
17
+ # @param [Boolean] overwrite existing code
18
+ # @return [Boolean]
19
+ def core_install(overwrite=true)
20
+ if core_installed? && overwrite == true
21
+ core_remove
22
+ end
23
+
24
+ log "Installing wordpress core"
25
+
26
+ unless ssh.directory_exists?(core_path)
27
+ ssh.run("mkdir -p #{core_path}")
28
+ end
29
+
30
+ result = ssh.run("cd #{core_path} && wp core download")
31
+
32
+ if result.success?
33
+ log "Wordpress core installed"
34
+ else
35
+ error "Unable to install wordpress core: #{result.output}"
36
+ end
37
+ end
38
+
39
+ # Remove wordpress shared core
40
+ # @return [Boolean]
41
+ def core_remove
42
+ if ssh.directory_exists?(core_path)
43
+ log "Removing wordpress shared core"
44
+ ssh.run("rm -rf #{core_path}")
45
+ end
46
+
47
+ ssh.directory_exists?(core_path)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,112 @@
1
+ module Shuttle
2
+ module WordpressPlugins
3
+ # Install wordpresss plugin
4
+ # @param [String] plugin name slug
5
+ def plugin_install(name)
6
+ log "Installing plugin: #{name}"
7
+
8
+ res = ssh.run("cd #{release_path} && wp plugin install #{name}")
9
+ if !res.success?
10
+ error "Unable to install plugin '#{name}'. Reason: #{res.output}"
11
+ end
12
+ end
13
+
14
+ def plugin_custom_install(name, url)
15
+ log "Installing custom plugin: #{name} -> #{url}"
16
+
17
+ if git_url?(url)
18
+ install_git_plugin(name, url)
19
+ elsif file_url?(url)
20
+ install_file_plugin(name, url)
21
+ else
22
+ error "Valid git URL or archive URL is required for plugin: #{name}"
23
+ end
24
+ end
25
+
26
+ # Check if wordpress plugin is installed
27
+ # @return [Boolean]
28
+ def plugin_installed?(name)
29
+ raise "Not Implemented"
30
+ end
31
+
32
+ private
33
+
34
+ # Check if provided plugin url is a git repository
35
+ # @return [Boolean]
36
+ def git_url?(url)
37
+ url.include?('.git') || url.include?('git@') || url.include?('git://')
38
+ end
39
+
40
+ # Check if provided plugin url is a file
41
+ # @return [Boolean]
42
+ def file_url?(url)
43
+ name = File.basename(url)
44
+ name.include?('.zip') || name.include?('.tar.gz')
45
+ end
46
+
47
+ def install_git_plugin(plugin_name, url)
48
+ ssh.run "cd #{release_path}/wp-content/plugins"
49
+ res = ssh.run "git clone #{url} #{plugin_name}"
50
+
51
+ if res.failure?
52
+ error "Unable to install plugin '#{plugin_name}'. Reason: #{res.output}"
53
+ end
54
+
55
+ # Init submodules if any
56
+ if ssh.file_exists?("#{release_path}/wp-content/plugins/#{plugin_name}/.gitmodules")
57
+ log "Initializing git submodules for #{plugin_name}"
58
+
59
+ res = ssh.run("cd #{plugin_name} && git submodule update --init --recursive")
60
+
61
+ if res.failure?
62
+ error "Unable to update submodules for #{plugin_name}: #{res.output}"
63
+ end
64
+ end
65
+
66
+ # Cleanup git folder
67
+ ssh.run("rm -rf #{release_path}/wp-content/plugins/#{plugin_name}/.git")
68
+ end
69
+
70
+ def install_file_plugin(plugin_name, url)
71
+ name = File.basename(url)
72
+ plugin_path = "#{release_path}/wp-content/plugins/"
73
+
74
+ if ssh.file_exists?("/tmp/#{name}")
75
+ ssh.run("rm -f /tmp/#{name}")
76
+ end
77
+
78
+ # Download file first
79
+ log "Downloading #{url}"
80
+ result = ssh.run("cd /tmp && wget #{url}")
81
+
82
+ if result.failure?
83
+ error "Unable to download file from #{url}"
84
+ end
85
+
86
+ log "Extracting #{name} to plugins directory"
87
+
88
+ if name.include?('.zip')
89
+ check_unzip
90
+
91
+ if ssh.run("unzip /tmp/#{name} -d #{plugin_path}").failure?
92
+ error "Unable to extract plugin"
93
+ end
94
+ elsif name.include?('.tar.gz')
95
+ if ssh.run("tar -xzf #{name} -C #{plugin_path}").failure?
96
+ error "Unable to extract plugin"
97
+ end
98
+ end
99
+ end
100
+
101
+ def check_unzip
102
+ if ssh.run("which unzip").failure?
103
+ log "Unzip utility is missing. Installing..."
104
+
105
+ ssh.run("sudo apt-get update")
106
+ if ssh.run("sudo apt-get -y install unzip").failure?
107
+ error "Unable to install unzip utility"
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,84 @@
1
+ module Shuttle
2
+ module WordpressVip
3
+ VIP_URL = "https://vip-svn.wordpress.com/plugins/"
4
+
5
+ # Get wordpress VIP shared path
6
+ # @return [String]
7
+ def vip_path
8
+ @vip_path ||= shared_path('wordpress/vip')
9
+ end
10
+
11
+ # Check if wordpress VIP is required
12
+ # @return [Boolean]
13
+ def vip_required?
14
+ !config.wordpress.vip.nil?
15
+ end
16
+
17
+ # Check if wordpress VIP is installed
18
+ # @return [Boolean]
19
+ def vip_installed?
20
+ ssh.directory_exists?(vip_path)
21
+ end
22
+
23
+ # Update wordpress VIP
24
+ def vip_update
25
+ if vip_installed?
26
+ ssh.run("rm -rf #{vip_path}")
27
+ end
28
+
29
+ vip_install
30
+ end
31
+
32
+ def vip_install
33
+ log "Installing wordpress VIP"
34
+
35
+ vip = vip_get_config
36
+
37
+ options = [
38
+ "--username #{vip.user}",
39
+ "--password #{vip.password}",
40
+ "--non-interactive",
41
+ VIP_URL,
42
+ vip_path
43
+ ].join(' ')
44
+
45
+ cmd = "svn co #{options}"
46
+
47
+ res = ssh.run(cmd, &method(:stream_output))
48
+
49
+ if res.success?
50
+ log "Wordpress VIP installed"
51
+ else
52
+ raise DeployError, "Unable to install wordpress VIP. Reason: #{res.output}"
53
+ end
54
+ end
55
+
56
+ def vip_get_config
57
+ data = config.wordpress.vip
58
+ if data.nil?
59
+ error "Please add VIP credentials to config."
60
+ end
61
+
62
+ if !data.user
63
+ error "VIP user is empty. Please set :user parameter"
64
+ end
65
+
66
+ if !data.password
67
+ error "VIP password is empty. Please set :password parameter"
68
+ end
69
+
70
+ data
71
+ end
72
+
73
+ def vip_link
74
+ ssh.run("mkdir -p #{release_path}/wp-content/themes/vip")
75
+ result = ssh.run("cp -a #{vip_path} #{release_path('wp-content/themes/vip/plugins')}")
76
+
77
+ if result.success?
78
+ log "Wordpress VIP is linked"
79
+ else
80
+ error "Unable to link VIP: #{result.output}"
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,191 @@
1
+ require 'shuttle/deployment/wordpress/core'
2
+ require 'shuttle/deployment/wordpress/cli'
3
+ require 'shuttle/deployment/wordpress/vip'
4
+ require 'shuttle/deployment/wordpress/plugins'
5
+
6
+ module Shuttle
7
+ class Wordpress < Php
8
+ include WordpressCli
9
+ include WordpressCore
10
+ include WordpressVip
11
+ include WordpressPlugins
12
+
13
+ def setup
14
+ if config.wordpress.nil?
15
+ error "Please add :wordpress section to your config"
16
+ end
17
+
18
+ super
19
+
20
+ setup_shared_dirs
21
+ check_dependencies
22
+ cli_install if !cli_installed?
23
+ core_install if !core_installed?
24
+ check_config
25
+ end
26
+
27
+ def deploy
28
+ setup
29
+ update_code
30
+ link_shared_data
31
+ checkout_theme
32
+
33
+ if vip_required?
34
+ vip_install if !vip_installed?
35
+ vip_link
36
+ end
37
+
38
+ if !site_installed?
39
+ site_install
40
+ network_install
41
+ end
42
+
43
+ check_plugins
44
+ activate_theme
45
+
46
+ link_release
47
+ end
48
+
49
+ def check_dependencies
50
+ if !svn_installed?
51
+ log "Installing Subversion"
52
+ if ssh.run("sudo apt-get install -y subversion").success?
53
+ log "Subversion installed"
54
+ end
55
+ end
56
+ end
57
+
58
+ def setup_shared_dirs
59
+ dirs = [
60
+ 'wordpress',
61
+ 'wordpress/uploads',
62
+ 'wordpress/core',
63
+ 'wordpress/plugins'
64
+ ]
65
+
66
+ dirs.each do |path|
67
+ ssh.run("mkdir -p #{shared_path(path)}")
68
+ end
69
+ end
70
+
71
+ def generate_config
72
+ mysql = config.wordpress.mysql
73
+ if mysql.nil?
74
+ error "Missing :mysql section of the config."
75
+ end
76
+
77
+ cmd = [
78
+ "wp core config",
79
+ "--dbname=#{mysql.database}",
80
+ "--dbhost=#{mysql.host || 'localhost'}",
81
+ "--dbuser=#{mysql.user}"
82
+ ]
83
+
84
+ cmd << "--dbpass=#{mysql.password}" if mysql.password
85
+
86
+ res = ssh.run("cd #{core_path} && #{cmd.join(' ')}")
87
+ if res.success?
88
+ log "A new wordpress config has been generated"
89
+ else
90
+ error "Unable to generate config"
91
+ end
92
+ end
93
+
94
+ def check_config
95
+ if !ssh.file_exists?(shared_path('wp-config.php'))
96
+ log "Creating wordpress config at 'shared/wp-config.php'"
97
+ generate_config
98
+ end
99
+ end
100
+
101
+ def site_installed?
102
+ ssh.run("cd #{release_path} && wp").success?
103
+ end
104
+
105
+ def site_install
106
+ if config.wordpress.site
107
+ site = config.wordpress.site
108
+
109
+ cmd = [
110
+ "wp core install",
111
+ "--url=#{site.url}",
112
+ "--title=#{site.title}",
113
+ "--admin_name=#{site.admin_name}",
114
+ "--admin_email=#{site.admin_email}",
115
+ "--admin_password=#{site.admin_password}"
116
+ ].join(' ')
117
+
118
+ result = ssh.run("cd #{release_path} && #{cmd}")
119
+ if result.failure?
120
+ error "Failed to setup site. #{result.output}"
121
+ end
122
+ else
123
+ error "Please define :site section"
124
+ end
125
+ end
126
+
127
+ def network_install
128
+ if config.wordpress.network
129
+ network = config.wordpress.network
130
+
131
+ cmd = [
132
+ "wp core install-network",
133
+ "--title=#{network.title}",
134
+ ].join(' ')
135
+
136
+ result = ssh.run("cd #{release_path} && #{cmd}")
137
+ if result.failure?
138
+ error "Failed to setup WP network. #{result.output}"
139
+ end
140
+ end
141
+ end
142
+
143
+ def link_shared_data
144
+ log "Linking shared data"
145
+
146
+ ssh.run("cp -a #{core_path} #{release_path}")
147
+ ssh.run("cp #{shared_path('wp-config.php')} #{release_path('wp-config.php')}")
148
+ ssh.run("ln -s #{shared_path('wordpress/uploads')} #{release_path('wp-content/uploads')}")
149
+ end
150
+
151
+ def check_plugins
152
+ plugins = config.wordpress.plugins
153
+
154
+ if plugins
155
+ if plugins.kind_of?(Array)
156
+ plugins.each do |p|
157
+ if p.kind_of?(String)
158
+ plugin_install(p)
159
+ elsif p.kind_of?(Hash)
160
+ name, url = p.to_a.flatten.map(&:to_s)
161
+ plugin_custom_install(name, url)
162
+ end
163
+ end
164
+ else
165
+ error "Config file has invalid plugins section"
166
+ end
167
+ end
168
+ end
169
+
170
+ def checkout_theme
171
+ if config.wordpress
172
+ if config.wordpress.theme
173
+ checkout_code("wp-content/themes/#{config.wordpress.theme}")
174
+ else
175
+ error "Theme name is not defined."
176
+ end
177
+ else
178
+ error "Config does not contain 'wordpress' section"
179
+ end
180
+ end
181
+
182
+ def activate_theme
183
+ name = config.wordpress.theme
184
+ result = ssh.run("cd #{release_path} && wp theme activate #{name}")
185
+
186
+ if result.failure?
187
+ error "Unable to activate theme. Error: #{result.output}"
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,5 @@
1
+ module Shuttle
2
+ class Error < StandardError ; end
3
+ class ConfigError < Error ; end
4
+ class DeployError < Error ; end
5
+ end
@@ -0,0 +1,50 @@
1
+ module Shuttle
2
+ module Helpers
3
+ LEVEL_COLORS = {
4
+ 'info' => :green,
5
+ 'warning' => :yellow,
6
+ 'error' => :red
7
+ }
8
+
9
+ def log(message, level='info')
10
+ prefix = "----->".send(LEVEL_COLORS[level])
11
+ STDOUT.puts("#{prefix} #{message}")
12
+ end
13
+
14
+ def error(message)
15
+ log("ERROR: #{message}", 'error')
16
+ raise DeployError, message
17
+ end
18
+
19
+ def git_installed?
20
+ ssh.run("which git").success?
21
+ end
22
+
23
+ def svn_installed?
24
+ ssh.run("which svn").success?
25
+ end
26
+
27
+ def release_exists?
28
+ ssh.directory_exists?(release_path)
29
+ end
30
+
31
+ def stream_output(buff)
32
+ str = buff.split("\n").map { |str| " #{str}"}.join("\n")
33
+ STDOUT.puts(str)
34
+ end
35
+
36
+ def git_remote
37
+ result = ssh.run("cd #{scm_path} && git remote -v")
38
+
39
+ if result.success?
40
+ result.output.scan(/^origin\t(.+)\s\(fetch\)/).flatten.first
41
+ else
42
+ nil
43
+ end
44
+ end
45
+
46
+ def deployer_hostname
47
+ `hostname`.strip
48
+ end
49
+ end
50
+ end