whiskey_disk 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/README +51 -0
- data/Rakefile +29 -0
- data/TODO.txt +3 -0
- data/VERSION +1 -0
- data/examples/deploy-staging.yml +6 -0
- data/examples/deploy.rake +11 -0
- data/examples/deploy.yml +12 -0
- data/init.rb +1 -0
- data/install.rb +5 -0
- data/lib/tasks/deploy.rb +36 -0
- data/lib/whiskey_disk/config.rb +77 -0
- data/lib/whiskey_disk.rb +122 -0
- data/spec/.bacon +0 -0
- data/spec/init_spec.rb +9 -0
- data/spec/install_spec.rb +43 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/tasks/deploy_spec.rb +277 -0
- data/spec/whiskey_disk/config_spec.rb +276 -0
- data/spec/whiskey_disk_spec.rb +387 -0
- metadata +88 -0
data/.gitignore
ADDED
data/README
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
Whiskey Disk -- embarrassingly fast deployments.
|
2
|
+
|
3
|
+
The idea here is inspired by github's cleaned up deployment scripts, and mislav's git-deploy project. Only, we gave up on capistrano a long time ago, and after a few years of doing deployments on a few dozen projects, we realized that we really have very little variation on how we do things. That is, we can afford to have a very opinionated tool to do our deployments.
|
4
|
+
|
5
|
+
Here are the features/constraints we're envisioning:
|
6
|
+
|
7
|
+
- We need some sort of very basic "run this on a remote server" functionality. We've been using vlad for years now and this would suffice: it does what we're looking for and is much much smaller than capistrano. If we don't load the included recipes it's basically a fancy ruby ssh wrapper.
|
8
|
+
|
9
|
+
- Setup should mostly just do a very fast remote git checkout in the right place. Deployment should very quickly update that checkout.
|
10
|
+
|
11
|
+
- We have been considering a move towards tracking configuration data across our projects as a separate concern. So, if we can have a private repo that stores per-project and per-environment (here I mean staging vs. production, etc.) configuration files, and have our deployments overlay those files quickly, that would be ideal. I'm talking about hoptoad configs, database.yml files, AWS cert files, GeoKit API keys, etc., etc., etc.
|
12
|
+
|
13
|
+
- We should be able to use the same "setup == clone" + "deploy == reset" technique to manage the per-project/per-environment config files.
|
14
|
+
|
15
|
+
- Using rsync on the remote to those overlay config files on the deployed project would be a fast way to get them in place.
|
16
|
+
|
17
|
+
- Get rid of a bunch of annoying symlinks and symlink-hoops-to-jump-through.
|
18
|
+
|
19
|
+
- Get rid of a bunch of space (yeah yeah disk is cheap, but copying isn't) on the disk devoted to umpteen "releases".
|
20
|
+
|
21
|
+
- Obviously reduce deployment time by doing less, ssh-ing less, and taking less time to do whatever.
|
22
|
+
|
23
|
+
- should be rake based, and should provide a bare minimum of tasks -- like deploy:setup, deploy:now, and maybe a deploy:refresh_config_files.
|
24
|
+
|
25
|
+
- While a very basic task or few would run after setup or after deployment (e.g., rake db:migrate if migrations were changed, or touch tmp/restart.txt if the web server needs a restart; see git-deploy for more examples), we should be able to declare optional rake tasks (e.g., "deploy:staging:post_deploy") and have them run on this project if they are declared.
|
26
|
+
|
27
|
+
- Should work with projects that aren't remotely ruby.
|
28
|
+
|
29
|
+
- Should be loadable as a gem, meaning that it doesn't need to live in your project's space. (see also non-ruby projects)
|
30
|
+
|
31
|
+
- Should be able to use a non-ruby config, preferably yaml, for information for all environments. That could be stored in <project>/config/deploy.yml and saved with the project. Even if this is just shoved into vlad 'set' commands, it's still an improvement: we don't need ruby in the config file because we're opinionated.
|
32
|
+
|
33
|
+
- should be able to override settings for an environment locally by declaring a <project>/config/deploy-<environment>.yml. Ideal for testing out deployments to different servers (or deploying locally). This also makes it possible to .gitignore your local settings, so everyone can have their config repos in different places.
|
34
|
+
|
35
|
+
- should make it easier to do local development (e.g., on a laptop) by being able to overlay config files using the same rake tasks as used for remote deployments, just not running the functionality remotely.
|
36
|
+
|
37
|
+
- dropping in a project Rakefile can add post-deploy / post-setup hooks transparently.
|
38
|
+
|
39
|
+
- actually have meaningful error messages, unlike anything that ever seems to happen with cap or vlad. :-/
|
40
|
+
|
41
|
+
- build this spec-first (whenever possible) so that there's a useful test suite.
|
42
|
+
|
43
|
+
- M$ windows hasn't been a priority for me for over a decade, not starting now.
|
44
|
+
|
45
|
+
---
|
46
|
+
|
47
|
+
Resources:
|
48
|
+
- http://github.com/blog/470-deployment-script-spring-cleaning
|
49
|
+
- http://github.com/mislav/git-deploy
|
50
|
+
- http://toroid.org/ams/git-website-howto
|
51
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
desc 'Default: run unit tests.'
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
desc 'Test RubyCloud'
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
9
|
+
t.libs << 'lib'
|
10
|
+
t.pattern = 'spec/**/*_spec.rb'
|
11
|
+
t.verbose = true
|
12
|
+
end
|
13
|
+
|
14
|
+
begin
|
15
|
+
require 'jeweler'
|
16
|
+
Jeweler::Tasks.new do |gemspec|
|
17
|
+
gemspec.name = "whiskey_disk"
|
18
|
+
gemspec.summary = "embarrassingly fast deployments."
|
19
|
+
gemspec.description = "Opinionated gem for doing fast git-based server deployments."
|
20
|
+
gemspec.email = "rick@rickbradley.com"
|
21
|
+
gemspec.homepage = "http://github.com/flogic/whiskey_disk"
|
22
|
+
gemspec.authors = ["Rick Bradley"]
|
23
|
+
gemspec.add_dependency('vlad', '>= 1.3.2')
|
24
|
+
end
|
25
|
+
Jeweler::GemcutterTasks.new
|
26
|
+
rescue LoadError
|
27
|
+
puts "Jeweler not available. Install it with: sudo gem install jeweler -s http://gemcutter.org"
|
28
|
+
end
|
29
|
+
|
data/TODO.txt
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
data/examples/deploy.yml
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
staging:
|
2
|
+
domain: "user@www.example.com"
|
3
|
+
deploy_to: "/var/www/suparsite.com/"
|
4
|
+
repository: "git://github.com/clarkkent/suparsite.git"
|
5
|
+
config_repository: "git@github.com:clarkkent/suparconfig.git"
|
6
|
+
deploy_config_to: "/var/cache/git/suparconfig"
|
7
|
+
branch: "production"
|
8
|
+
local:
|
9
|
+
repository: "git://github.com/clarkkent/suparsite.git"
|
10
|
+
config_repository: "git@github.com:clarkkent/suparconfig.git"
|
11
|
+
deploy_to: "/Users/clark/git/suparsite"
|
12
|
+
deploy_config_to: "/Users/clark/git/suparconfig"
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'lib', 'whiskey_disk'))
|
data/install.rb
ADDED
data/lib/tasks/deploy.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'whiskey_disk'))
|
2
|
+
require 'rake'
|
3
|
+
require 'vlad'
|
4
|
+
|
5
|
+
namespace :deploy do
|
6
|
+
desc "Perform initial setup for deployment"
|
7
|
+
task :setup do
|
8
|
+
WhiskeyDisk.ensure_main_parent_path_is_present if WhiskeyDisk.remote?
|
9
|
+
WhiskeyDisk.ensure_config_parent_path_is_present
|
10
|
+
WhiskeyDisk.checkout_main_repository if WhiskeyDisk.remote?
|
11
|
+
WhiskeyDisk.install_hooks if WhiskeyDisk.remote?
|
12
|
+
WhiskeyDisk.checkout_configuration_repository
|
13
|
+
WhiskeyDisk.refresh_configuration
|
14
|
+
WhiskeyDisk.run_post_setup_hooks
|
15
|
+
WhiskeyDisk.flush
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Deploy now."
|
19
|
+
task :now do
|
20
|
+
WhiskeyDisk.update_main_repository_checkout if WhiskeyDisk.remote?
|
21
|
+
WhiskeyDisk.update_configuration_repository_checkout
|
22
|
+
WhiskeyDisk.refresh_configuration
|
23
|
+
WhiskeyDisk.run_post_deploy_hooks
|
24
|
+
WhiskeyDisk.flush
|
25
|
+
end
|
26
|
+
|
27
|
+
task :post_setup do
|
28
|
+
env = WhiskeyDisk[:environment]
|
29
|
+
Rake::Task["deploy:#{env}:post_setup"].invoke if Rake::Task.task_defined? "deploy:#{env}:post_setup"
|
30
|
+
end
|
31
|
+
|
32
|
+
task :post_deploy do
|
33
|
+
env = WhiskeyDisk[:environment]
|
34
|
+
Rake::Task["deploy:#{env}:post_deploy"].invoke if Rake::Task.task_defined? "deploy:#{env}:post_deploy"
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class WhiskeyDisk
|
4
|
+
class Config
|
5
|
+
class << self
|
6
|
+
def environment_name
|
7
|
+
(ENV['to'] && ENV['to'] != '') ? ENV['to'] : false
|
8
|
+
end
|
9
|
+
|
10
|
+
def contains_rakefile?(path)
|
11
|
+
File.exists?(File.expand_path(File.join(path, 'Rakefile')))
|
12
|
+
end
|
13
|
+
|
14
|
+
def base_path
|
15
|
+
while (!contains_rakefile?(Dir.pwd))
|
16
|
+
raise "Could not find Rakefile in the current directory tree!" if Dir.pwd == '/'
|
17
|
+
Dir.chdir('..')
|
18
|
+
end
|
19
|
+
Dir.pwd
|
20
|
+
end
|
21
|
+
|
22
|
+
def main_configuration_file
|
23
|
+
File.expand_path(File.join(base_path, 'config', 'deploy.yml'))
|
24
|
+
end
|
25
|
+
|
26
|
+
def main_configuration_data
|
27
|
+
raise "Main configuration file [#{main_configuration_file}] not found!" unless File.exists?(main_configuration_file)
|
28
|
+
File.read(main_configuration_file)
|
29
|
+
end
|
30
|
+
|
31
|
+
def environment_configuration_file
|
32
|
+
raise "Cannot determine current environment -- try rake ... to=staging, for example." unless environment_name
|
33
|
+
File.expand_path(File.join(base_path, 'config', "deploy-#{environment_name}.yml"))
|
34
|
+
end
|
35
|
+
|
36
|
+
def environment_configuration_data
|
37
|
+
File.exists?(environment_configuration_file) ? File.read(environment_configuration_file) : nil
|
38
|
+
rescue Exception => e
|
39
|
+
raise %Q{Could not read configuration file [#{environment_configuration_file}] for environment [#{environment_name}]: "#{e}"}
|
40
|
+
end
|
41
|
+
|
42
|
+
def project_name(config)
|
43
|
+
return '' unless config['repository'] and config['repository'] != ''
|
44
|
+
config['repository'].sub(%r{^.*/}, '').sub(%r{\.git$}, '')
|
45
|
+
end
|
46
|
+
|
47
|
+
def load_main_data
|
48
|
+
YAML.load(main_configuration_data)
|
49
|
+
rescue Exception => e
|
50
|
+
raise %Q{Error reading configuration file [#{main_configuration_file}]: "#{e}"}
|
51
|
+
end
|
52
|
+
|
53
|
+
def load_environment_data
|
54
|
+
begin
|
55
|
+
env = environment_configuration_data ? YAML.load(environment_configuration_data) : nil
|
56
|
+
rescue Exception => e
|
57
|
+
raise %Q{Error reading configuration file [#{environment_configuration_file}]: "#{e}"}
|
58
|
+
end
|
59
|
+
raise "Configuration file [#{environment_configuration_file}] does not define data for environment [#{environment_name}]" if env and !env[environment_name]
|
60
|
+
env || {}
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch
|
64
|
+
raise "Cannot determine current environment -- try rake ... to=staging, for example." unless environment_name
|
65
|
+
main, env = load_main_data, load_environment_data
|
66
|
+
raise "No configuration file defined data for environment [#{environment_name}]" unless main[environment_name] or env[environment_name]
|
67
|
+
config = (main[environment_name] || {}).merge(env[environment_name] || {}).merge({'environment' => environment_name})
|
68
|
+
{ 'project' => project_name(config) }.merge(config)
|
69
|
+
end
|
70
|
+
|
71
|
+
def filenames
|
72
|
+
raise "Cannot determine current environment -- try rake ... to=staging, for example." unless environment_name
|
73
|
+
[ main_configuration_file, environment_configuration_file ]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/whiskey_disk.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'tasks', 'deploy'))
|
2
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'whiskey_disk', 'config'))
|
3
|
+
|
4
|
+
class WhiskeyDisk
|
5
|
+
class << self
|
6
|
+
def reset
|
7
|
+
@configuration = nil
|
8
|
+
@buffer = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def buffer
|
12
|
+
@buffer ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def configuration
|
16
|
+
@configuration ||= WhiskeyDisk::Config.fetch
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](key)
|
20
|
+
configuration[key.to_s]
|
21
|
+
end
|
22
|
+
|
23
|
+
def enqueue(command)
|
24
|
+
buffer << command
|
25
|
+
end
|
26
|
+
|
27
|
+
def remote?
|
28
|
+
! (self[:domain].nil? or self[:domain] == '')
|
29
|
+
end
|
30
|
+
|
31
|
+
def parent_path(path)
|
32
|
+
File.split(path).first
|
33
|
+
end
|
34
|
+
|
35
|
+
def tail_path(path)
|
36
|
+
File.split(path).last
|
37
|
+
end
|
38
|
+
|
39
|
+
def register_configuration
|
40
|
+
configuration.each_pair {|k,v| set k, v }
|
41
|
+
end
|
42
|
+
|
43
|
+
def needs(*keys)
|
44
|
+
keys.each do |key|
|
45
|
+
raise "No value for '#{key}' declared in configuration files [#{WhiskeyDisk::Config.filenames.join(", ")}]" unless self[key]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def bundle
|
50
|
+
return '' if buffer.empty?
|
51
|
+
buffer.collect {|c| "(#{c})" }.join(' && ')
|
52
|
+
end
|
53
|
+
|
54
|
+
def flush
|
55
|
+
if remote?
|
56
|
+
register_configuration
|
57
|
+
run(bundle)
|
58
|
+
else
|
59
|
+
system(bundle)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def ensure_main_parent_path_is_present
|
64
|
+
needs(:deploy_to)
|
65
|
+
enqueue "mkdir -p #{parent_path(self[:deploy_to])}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def ensure_config_parent_path_is_present
|
69
|
+
needs(:deploy_config_to)
|
70
|
+
enqueue "mkdir -p #{parent_path(self[:deploy_config_to])}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def checkout_main_repository
|
74
|
+
needs(:deploy_to, :repository)
|
75
|
+
enqueue "cd #{parent_path(self[:deploy_to])}"
|
76
|
+
enqueue "git clone #{self[:repository]} #{tail_path(self[:deploy_to])} || true"
|
77
|
+
end
|
78
|
+
|
79
|
+
def install_hooks
|
80
|
+
needs(:deploy_to)
|
81
|
+
# FIXME - TODO: MORE HERE
|
82
|
+
end
|
83
|
+
|
84
|
+
def checkout_configuration_repository
|
85
|
+
needs(:deploy_config_to, :config_repository)
|
86
|
+
enqueue "cd #{parent_path(self[:deploy_config_to])}"
|
87
|
+
enqueue "git clone #{self[:config_repository]} #{tail_path(self[:deploy_config_to])} || true"
|
88
|
+
end
|
89
|
+
|
90
|
+
def update_main_repository_checkout
|
91
|
+
needs(:deploy_to)
|
92
|
+
branch = (self[:branch] and self[:branch] != '') ? self[:branch] : 'master'
|
93
|
+
enqueue "cd #{self[:deploy_to]}"
|
94
|
+
enqueue "git fetch origin +refs/heads/#{branch}:refs/remotes/origin/#{branch}"
|
95
|
+
enqueue "git reset --hard origin/#{branch}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def update_configuration_repository_checkout
|
99
|
+
needs(:deploy_config_to)
|
100
|
+
enqueue "cd #{self[:deploy_config_to]}"
|
101
|
+
enqueue "git fetch origin +refs/heads/master:refs/remotes/origin/master"
|
102
|
+
enqueue "git reset --hard origin/master"
|
103
|
+
end
|
104
|
+
|
105
|
+
def refresh_configuration
|
106
|
+
needs(:deploy_to, :deploy_config_to)
|
107
|
+
enqueue "rsync -av --progress #{self[:deploy_config_to]}/#{self[:project]}/#{self[:environment]}/ #{self[:deploy_to]}/"
|
108
|
+
end
|
109
|
+
|
110
|
+
def run_post_setup_hooks
|
111
|
+
needs(:deploy_to)
|
112
|
+
enqueue "cd #{self[:deploy_to]}"
|
113
|
+
enqueue "rake deploy:post_setup"
|
114
|
+
end
|
115
|
+
|
116
|
+
def run_post_deploy_hooks
|
117
|
+
needs(:deploy_to)
|
118
|
+
enqueue "cd #{self[:deploy_to]}"
|
119
|
+
enqueue "rake deploy:post_deploy"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
data/spec/.bacon
ADDED
File without changes
|
data/spec/init_spec.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper.rb'))
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
describe 'when the init.rb plugin loader has been included' do
|
5
|
+
it 'should load the main library' do
|
6
|
+
require(File.expand_path(File.join(File.dirname(__FILE__), '..', 'init')))
|
7
|
+
$".should.include(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'whiskey_disk.rb')))
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
|
2
|
+
|
3
|
+
def do_install
|
4
|
+
eval File.read(File.join(File.dirname(__FILE__), *%w[.. install.rb ]))
|
5
|
+
end
|
6
|
+
|
7
|
+
describe 'the plugin install.rb script' do
|
8
|
+
before do
|
9
|
+
self.stub!(:puts).and_return(true)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'displays the content of the plugin README file' do
|
13
|
+
self.stub!(:readme_contents).and_return('README CONTENTS')
|
14
|
+
self.should.receive(:puts).with('README CONTENTS')
|
15
|
+
do_install
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'readme_contents' do
|
19
|
+
it 'should work without arguments' do
|
20
|
+
do_install
|
21
|
+
lambda { readme_contents }.should.not.raise(ArgumentError)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should accept no arguments' do
|
25
|
+
do_install
|
26
|
+
lambda { readme_contents(:foo) }.should.raise(ArgumentError)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should read the plugin README file' do
|
30
|
+
do_install
|
31
|
+
File.stub!(:join).and_return('/path/to/README')
|
32
|
+
IO.should.receive(:read).with('/path/to/README')
|
33
|
+
readme_contents
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should return the contents of the plugin README file' do
|
37
|
+
do_install
|
38
|
+
File.stub!(:join).and_return('/path/to/README')
|
39
|
+
IO.stub!(:read).with('/path/to/README').and_return('README CONTENTS')
|
40
|
+
readme_contents.should == 'README CONTENTS'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|