shuttle-deploy 0.2.0.beta1

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.
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