hsume2-cap-taffy 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +9 -0
- data/README.md +83 -0
- data/Rakefile +34 -0
- data/cap-taffy.gemspec +45 -0
- data/lib/cap-taffy/db.rb +239 -0
- data/lib/cap-taffy/parse.rb +53 -0
- data/lib/cap-taffy/ssh.rb +18 -0
- data/lib/cap-taffy.rb +45 -0
- data/spec/cap-taffy/db_spec.rb +287 -0
- data/spec/cap-taffy/parse_spec.rb +57 -0
- data/spec/cap-taffy/ssh_spec.rb +37 -0
- data/spec/cap-taffy_spec.rb +4 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +179 -0
- metadata +117 -0
data/History.txt
ADDED
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
Capistrano Taffy (Database, SSH recipes)
|
2
|
+
================================================
|
3
|
+
|
4
|
+
**Capistrano recipes for deploying databases and other common tasks (managing database.yml, importing/exporting/transfering databases, SSH authorization etc.)**
|
5
|
+
|
6
|
+
Features
|
7
|
+
------------------------------------------------
|
8
|
+
|
9
|
+
* Adds database transfer recipes (via [`Taps`]("http://github.com/ricardochimal/taps"))
|
10
|
+
* Authorize SSH access
|
11
|
+
* Manage `database.yml` (Soon.)
|
12
|
+
|
13
|
+
[`Taps`]("http://github.com/ricardochimal/taps") is great, but having to SSH into my deployment to run the `Taps` server, as well as
|
14
|
+
figure out the proper local/remote database urls, is a pain. I knew the [`Heroku`]("http://github.com/heroku/heroku") gem
|
15
|
+
had it figured out; I present the Capistrano friendly version.
|
16
|
+
|
17
|
+
If you are new to `Taps`, check out this [introduction to `Taps`]("http://adam.blog.heroku.com/past/2009/2/11/taps_for_easy_database_transfers/") by [Adam Wiggins]("http://github.com/adamwiggins").
|
18
|
+
|
19
|
+
Installation
|
20
|
+
------------------------------------------------
|
21
|
+
|
22
|
+
gem install cap-taffy
|
23
|
+
|
24
|
+
Database Transfer
|
25
|
+
------------------------------------------------
|
26
|
+
|
27
|
+
> _Dependency:_ The [`Taps`]("http://github.com/ricardochimal/taps") gem is required on any server(s) you'll be transferring databases to (`:app` role) including your development machine (where you'll be running `cap` tasks from). Run:
|
28
|
+
|
29
|
+
> gem install taps
|
30
|
+
|
31
|
+
> _Dependency:_ The [`Heroku`]("http://github.com/heroku/heroku") gem is required on your development machine.
|
32
|
+
|
33
|
+
> gem install heroku
|
34
|
+
|
35
|
+
### Usage
|
36
|
+
|
37
|
+
To start, add the following to your `Capfile`
|
38
|
+
|
39
|
+
require 'cap-taffy/db'
|
40
|
+
|
41
|
+
`Taffy` will start a `Taps` server on port 5000 by default, so make sure port 5000 (or w/e port you end up using) is open on your `:app` server(s)
|
42
|
+
|
43
|
+
Then you can use:
|
44
|
+
|
45
|
+
cap db:push # Or to specify a different port: cap db:push -s taps_port=4321
|
46
|
+
cap db:pull # Or to specify a different port: cap db:pull -s taps_port=4321
|
47
|
+
|
48
|
+
|
49
|
+
#### SSH Local Forwarding
|
50
|
+
|
51
|
+
> Some of you may not be able/want to open up ports on your servers. Instead, you can run:
|
52
|
+
|
53
|
+
> ssh -N -L[port]:127.0.0.1:[port] [user]@[remote-server]
|
54
|
+
> And then run:
|
55
|
+
|
56
|
+
> cap db:push -s taps_port=[port] -s local=true
|
57
|
+
> Substituing the appropriate values for [port], [user], and [remote-server].
|
58
|
+
> #### Sample Usage
|
59
|
+
> > ssh -N -L4321:127.0.0.1:4321 henry@load-test
|
60
|
+
> > cap db:push -s taps_port=4321 -s local=true
|
61
|
+
|
62
|
+
SSH Access
|
63
|
+
------------------------------------------------
|
64
|
+
|
65
|
+
#### Usage
|
66
|
+
|
67
|
+
Add the following to your `Capfile`
|
68
|
+
|
69
|
+
require 'cap-taffy/ssh'
|
70
|
+
|
71
|
+
Using a public key generated from `ssh-keygen` (e.g. `ssh-keygen -t rsa`), to authorize access:
|
72
|
+
|
73
|
+
cap ssh:authorize # authorizes local public key for SSH access to remote server(s)
|
74
|
+
|
75
|
+
|
76
|
+
Managing `database.yml`
|
77
|
+
------------------------------------------------
|
78
|
+
|
79
|
+
> Much needed and coming soon.
|
80
|
+
|
81
|
+
Credits
|
82
|
+
------------------------------------------------
|
83
|
+
Thanks to developers of the [`Taps`]("http://adam.blog.heroku.com/past/2009/2/11/taps_for_easy_database_transfers/") gem and the [`Heroku`]("http://github.com/heroku/heroku") gem. And of course, Capistrano, awesome!
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Look in the tasks/setup.rb file for the various options that can be
|
2
|
+
# configured in this Rakefile. The .rake files in the tasks directory
|
3
|
+
# are where the options are used.
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bones'
|
7
|
+
Bones.setup
|
8
|
+
rescue LoadError
|
9
|
+
begin
|
10
|
+
load 'tasks/setup.rb'
|
11
|
+
rescue LoadError
|
12
|
+
raise RuntimeError, '### please install the "bones" gem ###'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
ensure_in_path 'lib'
|
17
|
+
require 'cap-taffy'
|
18
|
+
|
19
|
+
task :default => 'spec:run'
|
20
|
+
|
21
|
+
PROJ.name = 'cap-taffy'
|
22
|
+
PROJ.authors = 'Henry Hsu'
|
23
|
+
PROJ.email = 'henry@qlane.com'
|
24
|
+
PROJ.url = 'http://by.qlane.com'
|
25
|
+
PROJ.version = CapTaffy::VERSION
|
26
|
+
PROJ.rubyforge.name = 'cap-taffy'
|
27
|
+
PROJ.readme_file = "README.md"
|
28
|
+
PROJ.gem.dependencies = ['heroku', 'taps', 'capistrano']
|
29
|
+
PROJ.gem.development_dependencies << ["mocha"]
|
30
|
+
PROJ.description = "Capistrano recipes for deploying databases and other common tasks."
|
31
|
+
PROJ.summary = "Capistrano recipes for deploying databases (managing database.yml, importing/exporting/transfering databases, etc.)"
|
32
|
+
PROJ.ignore_file = '.gitignore'
|
33
|
+
PROJ.spec.opts << '--color'
|
34
|
+
PROJ.exclude << "bin"
|
data/cap-taffy.gemspec
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{cap-taffy}
|
5
|
+
s.version = "0.0.2"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Henry Hsu"]
|
9
|
+
s.date = %q{2009-09-17}
|
10
|
+
s.description = %q{Capistrano recipes for deploying databases and other common tasks.}
|
11
|
+
s.email = %q{henry@qlane.com}
|
12
|
+
s.extra_rdoc_files = ["History.txt"]
|
13
|
+
s.files = ["History.txt", "README.md", "Rakefile", "cap-taffy.gemspec", "lib/cap-taffy.rb", "lib/cap-taffy/db.rb", "lib/cap-taffy/parse.rb", "lib/cap-taffy/ssh.rb", "spec/cap-taffy/db_spec.rb", "spec/cap-taffy/parse_spec.rb", "spec/cap-taffy/ssh_spec.rb", "spec/cap-taffy_spec.rb", "spec/spec.opts", "spec/spec_helper.rb"]
|
14
|
+
s.homepage = %q{http://by.qlane.com}
|
15
|
+
s.rdoc_options = ["--main", "README.md"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = %q{cap-taffy}
|
18
|
+
s.rubygems_version = %q{1.3.5}
|
19
|
+
s.summary = %q{Capistrano recipes for deploying databases (managing database.yml, importing/exporting/transfering databases, etc.)}
|
20
|
+
|
21
|
+
if s.respond_to? :specification_version then
|
22
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
23
|
+
s.specification_version = 3
|
24
|
+
|
25
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
26
|
+
s.add_runtime_dependency(%q<heroku>, [">= 0"])
|
27
|
+
s.add_runtime_dependency(%q<taps>, [">= 0"])
|
28
|
+
s.add_runtime_dependency(%q<capistrano>, [">= 0"])
|
29
|
+
s.add_development_dependency(%q<bones>, [">= 2.5.1"])
|
30
|
+
s.add_development_dependency(%q<mocha>, [">= 0"])
|
31
|
+
else
|
32
|
+
s.add_dependency(%q<heroku>, [">= 0"])
|
33
|
+
s.add_dependency(%q<taps>, [">= 0"])
|
34
|
+
s.add_dependency(%q<capistrano>, [">= 0"])
|
35
|
+
s.add_dependency(%q<bones>, [">= 2.5.1"])
|
36
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
37
|
+
end
|
38
|
+
else
|
39
|
+
s.add_dependency(%q<heroku>, [">= 0"])
|
40
|
+
s.add_dependency(%q<taps>, [">= 0"])
|
41
|
+
s.add_dependency(%q<capistrano>, [">= 0"])
|
42
|
+
s.add_dependency(%q<bones>, [">= 2.5.1"])
|
43
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
44
|
+
end
|
45
|
+
end
|
data/lib/cap-taffy/db.rb
ADDED
@@ -0,0 +1,239 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
begin
|
3
|
+
gem 'taps', '>= 0.2.8', '< 0.3.0'
|
4
|
+
require 'taps/client_session'
|
5
|
+
rescue LoadError
|
6
|
+
error "Install the Taps gem to use db commands. On most systems this will be:\nsudo gem install taps"
|
7
|
+
end
|
8
|
+
|
9
|
+
require File.join(File.dirname(__FILE__), %w[.. cap-taffy]) unless defined?(CapTaffy)
|
10
|
+
require File.join(File.dirname(__FILE__), 'parse')
|
11
|
+
require 'digest/sha1'
|
12
|
+
|
13
|
+
module CapTaffy::Db
|
14
|
+
extend self
|
15
|
+
|
16
|
+
# Detects the local database url for +env+.
|
17
|
+
#
|
18
|
+
# Looks for <tt>config/database.yml</tt>.
|
19
|
+
def local_database_url(env)
|
20
|
+
return "" unless File.exists?(Dir.pwd + '/config/database.yml')
|
21
|
+
db_config = YAML.load(File.read(Dir.pwd + '/config/database.yml'))
|
22
|
+
|
23
|
+
CapTaffy::Parse.database_url(db_config, env)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Detects the remote database url for +env+ and the current Capistrano +instance+.
|
27
|
+
#
|
28
|
+
# Looks for <tt>config/database.yml</tt> in the +current_path+.
|
29
|
+
def remote_database_url(instance, env)
|
30
|
+
db_yml = instance.capture "cat #{instance.current_path}/config/database.yml"
|
31
|
+
db_config = YAML::load(db_yml)
|
32
|
+
|
33
|
+
CapTaffy::Parse.database_url(db_config, env)
|
34
|
+
end
|
35
|
+
|
36
|
+
# The default server port the Taps server is started on.
|
37
|
+
def default_server_port
|
38
|
+
5000
|
39
|
+
end
|
40
|
+
|
41
|
+
# Generates the remote url used by Taps push/pull.
|
42
|
+
#
|
43
|
+
# ==== Parameters
|
44
|
+
#
|
45
|
+
# * <tt>:login, :password, :host, :port</tt> - See #run.
|
46
|
+
#
|
47
|
+
# ==== Examples
|
48
|
+
#
|
49
|
+
# login = fetch(:user)
|
50
|
+
# password = tmp_pass(login) # returns asdkf239udjhdaks (for example)
|
51
|
+
# remote_url(:login => login, :password => password, :host => 'load-test') # returns http://henry:asdkf239udjhdaks@load-test:5000
|
52
|
+
def remote_url(options={})
|
53
|
+
host = options[:host]
|
54
|
+
port = options[:port] || default_server_port
|
55
|
+
host += ":#{port}"
|
56
|
+
url = CapTaffy::Parse.new.uri_hash_to_url('username' => options[:login], 'password' => options[:password], 'host' => host, 'scheme' => 'http', 'path' => '')
|
57
|
+
|
58
|
+
url.sub(/\/$/, '')
|
59
|
+
end
|
60
|
+
|
61
|
+
# Generates a temporary password to be used for the Taps server command.
|
62
|
+
def tmp_pass(user)
|
63
|
+
Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{user}--")
|
64
|
+
end
|
65
|
+
|
66
|
+
# A quick start for a Taps client.
|
67
|
+
#
|
68
|
+
# <tt>local_database_url</tt> and <tt>remote_url</tt> refer to the options for the Taps gem (see #run).
|
69
|
+
def taps_client(local_database_url, remote_url, &blk) # :yields: client
|
70
|
+
Taps::Config.chunksize = 1000
|
71
|
+
Taps::Config.database_url = local_database_url
|
72
|
+
Taps::Config.remote_url = remote_url
|
73
|
+
Taps::Config.verify_database_url
|
74
|
+
|
75
|
+
Taps::ClientSession.quickstart do |client|
|
76
|
+
yield client
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Generates the server command used to start a Taps server
|
81
|
+
#
|
82
|
+
# ==== Parameters
|
83
|
+
# * <tt>:remote_database_url, :login, :password</tt> - See #run.
|
84
|
+
# * <tt>:port</tt> - The +port+ the Taps server is on. If given and different from #default_server_port, appends <tt>--port=[port]</tt> to command.
|
85
|
+
def server_command(options={})
|
86
|
+
remote_database_url, login, password, port = options[:remote_database_url], options[:login], options[:password], options[:port]
|
87
|
+
port_argument = ''
|
88
|
+
port_argument = " --port=#{port}" if port && port != default_server_port
|
89
|
+
|
90
|
+
"taps server #{remote_database_url} #{login} #{password}#{port_argument}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# The meat of the operation. Runs operations after setting up the Taps server.
|
94
|
+
#
|
95
|
+
# 1. Runs the <tt>taps</tt> taps command to start the Taps server (assuming Sinatra is running on Thin)
|
96
|
+
# 2. Wait until the server is ready
|
97
|
+
# 3. Execute block on Taps client
|
98
|
+
# 4. Close the connection(s) and bid farewell.
|
99
|
+
#
|
100
|
+
# ==== Parameters
|
101
|
+
# * <tt>:remote_database_url</tt> - Refers to local database url in the options for the Taps server command (see Taps Options).
|
102
|
+
# * <tt>:login</tt> - The login for +host+. Usually what's in <tt>set :user, "the user"</tt> in <tt>deploy.rb</tt>
|
103
|
+
# * <tt>:password</tt> - The temporary password for the Taps server.
|
104
|
+
# * <tt>:port</tt> - The +port+ the Taps server is on. If not given, defaults to #default_server_port.
|
105
|
+
# * <tt>:local_database_url</tt> - Refers to the local database url in the options for Taps client commands (see Taps Options).
|
106
|
+
#
|
107
|
+
# ==== Taps Options
|
108
|
+
#
|
109
|
+
# <tt>taps</tt>
|
110
|
+
# server <local_database_url> <login> <password> [--port=N] Start a taps database import/export server
|
111
|
+
# pull <local_database_url> <remote_url> [--chunksize=N] Pull a database from a taps server
|
112
|
+
# push <local_database_url> <remote_url> [--chunksize=N] Push a database to a taps server
|
113
|
+
#
|
114
|
+
# ==== Examples
|
115
|
+
#
|
116
|
+
# task :push do
|
117
|
+
# login = fetch(:user)
|
118
|
+
# password = Time.now.to_s
|
119
|
+
# CapTaffy.Db.run(self, { :login => login, :password => password, :remote_database_url => "sqlite://test_production", :local_database_url => "sqlite://test_development" }) do |client|
|
120
|
+
# client.cmd_send
|
121
|
+
# end
|
122
|
+
# end
|
123
|
+
def run(instance, options = {} , &blk) # :yields: client
|
124
|
+
options[:port] ||= default_server_port
|
125
|
+
remote_database_url, login, password, port, local_database_url = options[:remote_database_url], options[:login], options[:password], options[:port], options[:local_database_url]
|
126
|
+
force_local = options.delete(:local)
|
127
|
+
|
128
|
+
data_so_far = ""
|
129
|
+
instance.run CapTaffy::Db.server_command(options) do |channel, stream, data|
|
130
|
+
data_so_far << data
|
131
|
+
if data_so_far.include? ">> Listening on 0.0.0.0:#{port}, CTRL+C to stop"
|
132
|
+
host = force_local ? '127.0.0.1' : channel[:host]
|
133
|
+
remote_url = CapTaffy::Db.remote_url(options.merge(:host => host))
|
134
|
+
|
135
|
+
CapTaffy::Db.taps_client(local_database_url, remote_url) do |client|
|
136
|
+
yield client
|
137
|
+
end
|
138
|
+
|
139
|
+
data_so_far = ""
|
140
|
+
channel.close
|
141
|
+
channel[:status] = 0
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class InvalidURL < RuntimeError # :nodoc:
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
Capistrano::Configuration.instance.load do
|
151
|
+
namespace :db do
|
152
|
+
# Executes given block.
|
153
|
+
# If this is a dry run, any raised exceptions will be caught and +returning+ is returned.
|
154
|
+
# If this is not a dry run, any exceptions will be raised as expected.
|
155
|
+
def dry_run_safe(returning = nil, &block) # :yields:
|
156
|
+
begin
|
157
|
+
yield
|
158
|
+
rescue Exception => e
|
159
|
+
raise e unless dry_run
|
160
|
+
return returning
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
task :detect, :roles => :app do
|
165
|
+
@remote_database_url = dry_run_safe('') { CapTaffy::Db.remote_database_url(self, 'production') }
|
166
|
+
@local_database_url = dry_run_safe('') { CapTaffy::Db.local_database_url('development') }
|
167
|
+
end
|
168
|
+
|
169
|
+
desc <<-DESC
|
170
|
+
Push a local database into the app's remote database.
|
171
|
+
|
172
|
+
Performs push from local development database to remote production database.
|
173
|
+
Opens a Taps server on port 5000. (Ensure port is opened on the remote server).
|
174
|
+
|
175
|
+
# alternately, specify a different port
|
176
|
+
cap db:push -s taps_port=4321
|
177
|
+
|
178
|
+
For the security conscious:
|
179
|
+
|
180
|
+
# use ssh local forwarding (ensure [port] is available on both endpoints)
|
181
|
+
ssh -N -L[port]:127.0.0.1:[port] [user]@[remote-server]
|
182
|
+
|
183
|
+
# then push locally
|
184
|
+
cap db:push -s taps_port=[port] -s local=true
|
185
|
+
DESC
|
186
|
+
task :push, :roles => :app do
|
187
|
+
detect
|
188
|
+
|
189
|
+
login = fetch(:user)
|
190
|
+
password = CapTaffy::Db.tmp_pass(login)
|
191
|
+
|
192
|
+
logger = Capistrano::Logger.new
|
193
|
+
logger.important "Auto-detected remote database: #{@remote_database_url}" if @remote_database_url != ''
|
194
|
+
logger.important "Auto-detected local database: #{@local_database_url}" if @local_database_url != ''
|
195
|
+
|
196
|
+
options = {:remote_database_url => @remote_database_url, :login => login, :password => password, :local_database_url => @local_database_url, :port => variables[:taps_port]}
|
197
|
+
options.merge!(:local => true) if variables[:local]
|
198
|
+
|
199
|
+
CapTaffy::Db.run(self, options) do |client|
|
200
|
+
client.cmd_send
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
desc <<-DESC
|
205
|
+
Pull the app's database into a local database.
|
206
|
+
|
207
|
+
Performs pull from remote production database to local development database.
|
208
|
+
Opens a Taps server on port 5000. (Ensure port is opened on the remote server).
|
209
|
+
|
210
|
+
# alternately, specify a different port
|
211
|
+
cap db:pull -s taps_port=4321
|
212
|
+
|
213
|
+
For the security conscious:
|
214
|
+
|
215
|
+
# use ssh local forwarding (ensure [port] is available on both endpoints)
|
216
|
+
ssh -N -L[port]:127.0.0.1:[port] [user]@[remote-server]
|
217
|
+
|
218
|
+
# then pull locally
|
219
|
+
cap db:pull -s taps_port=[port] -s local=true
|
220
|
+
DESC
|
221
|
+
task :pull, :roles => :app do
|
222
|
+
detect
|
223
|
+
|
224
|
+
login = fetch(:user)
|
225
|
+
password = CapTaffy::Db.tmp_pass(login)
|
226
|
+
|
227
|
+
logger = Capistrano::Logger.new
|
228
|
+
logger.important "Auto-detected remote database: #{@remote_database_url}" if @remote_database_url != ''
|
229
|
+
logger.important "Auto-detected local database: #{@local_database_url}" if @local_database_url != ''
|
230
|
+
|
231
|
+
options = {:remote_database_url => @remote_database_url, :login => login, :password => password, :local_database_url => @local_database_url, :port => variables[:taps_port]}
|
232
|
+
options.merge!(:local => true) if variables[:local]
|
233
|
+
|
234
|
+
CapTaffy::Db.run(self, options) do |client|
|
235
|
+
client.cmd_receive
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
begin
|
3
|
+
require 'heroku'
|
4
|
+
require 'heroku/commands/base'
|
5
|
+
require 'heroku/commands/db'
|
6
|
+
rescue LoadError
|
7
|
+
error "Install the Heroku gem. On most systems this will be:\nsudo gem install taps"
|
8
|
+
end
|
9
|
+
|
10
|
+
module CapTaffy
|
11
|
+
class Parse < Heroku::Command::Db
|
12
|
+
class << self
|
13
|
+
attr_accessor :instance
|
14
|
+
|
15
|
+
# Modified from :parse_database_yml in heroku/command/db.rb
|
16
|
+
#
|
17
|
+
# Accepts a complete +db_config+ hash and +env+ and parses for a database_url accordingly.
|
18
|
+
def database_url(db_config, env)
|
19
|
+
raise Invalid, "please pass me a valid Hash loaded from a database YAML file" unless db_config
|
20
|
+
conf = db_config[env]
|
21
|
+
raise Invalid, "missing '#{env}' database in #{db_config.inspect}" unless conf
|
22
|
+
|
23
|
+
self.instance ||= CapTaffy::Parse.new
|
24
|
+
|
25
|
+
case conf['adapter']
|
26
|
+
when 'sqlite3'
|
27
|
+
return "sqlite://#{conf['database']}"
|
28
|
+
when 'postgresql'
|
29
|
+
uri_hash = self.instance.conf_to_uri_hash(conf)
|
30
|
+
uri_hash['scheme'] = 'postgres'
|
31
|
+
return self.instance.uri_hash_to_url(uri_hash)
|
32
|
+
else
|
33
|
+
return self.instance.uri_hash_to_url(self.instance.conf_to_uri_hash(conf))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Override to do nothing on #new
|
39
|
+
def initialize # :nodoc:
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# Override to pass-through on #escape
|
44
|
+
def escape(string)
|
45
|
+
string
|
46
|
+
end
|
47
|
+
|
48
|
+
public :uri_hash_to_url, :conf_to_uri_hash
|
49
|
+
|
50
|
+
class Invalid < RuntimeError # :nodoc:
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[.. cap-taffy]) unless defined?(CapTaffy)
|
2
|
+
|
3
|
+
module CapTaffy::SSH
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
Capistrano::Configuration.instance.load do
|
8
|
+
namespace :ssh do
|
9
|
+
desc <<-DESC
|
10
|
+
Authorize SSH access for local computer on remote computers(s).
|
11
|
+
DESC
|
12
|
+
task :authorize do
|
13
|
+
public_key = File.read(File.expand_path(File.join(%w[~/ .ssh id_rsa.pub]))).chop
|
14
|
+
|
15
|
+
run %Q[if [ ! -f ~/.ssh/authorized_keys ]; then mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys; fi && if [ -z "$(grep "^#{public_key}$" ~/.ssh/authorized_keys)" ]; then echo "#{public_key}" >> ~/.ssh/authorized_keys && echo "Public key on '$CAPISTRANO:HOST$' authorized at '#{Time.now.to_s}'"; else echo "Public key on '$CAPISTRANO:HOST$' is already authorized."; fi]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/cap-taffy.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
module CapTaffy
|
3
|
+
|
4
|
+
# :stopdoc:
|
5
|
+
VERSION = '0.0.2'
|
6
|
+
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
7
|
+
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
8
|
+
# :startdoc:
|
9
|
+
|
10
|
+
# Returns the version string for the library.
|
11
|
+
#
|
12
|
+
def self.version
|
13
|
+
VERSION
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the library path for the module. If any arguments are given,
|
17
|
+
# they will be joined to the end of the libray path using
|
18
|
+
# <tt>File.join</tt>.
|
19
|
+
#
|
20
|
+
def self.libpath( *args )
|
21
|
+
args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the lpath for the module. If any arguments are given,
|
25
|
+
# they will be joined to the end of the path using
|
26
|
+
# <tt>File.join</tt>.
|
27
|
+
#
|
28
|
+
def self.path( *args )
|
29
|
+
args.empty? ? PATH : ::File.join(PATH, args.flatten)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Utility method used to require all files ending in .rb that lie in the
|
33
|
+
# directory below this file that has the same name as the filename passed
|
34
|
+
# in. Optionally, a specific _directory_ name can be passed in such that
|
35
|
+
# the _filename_ does not have to be equivalent to the directory.
|
36
|
+
#
|
37
|
+
def self.require_all_libs_relative_to( fname, dir = nil )
|
38
|
+
dir ||= ::File.basename(fname, '.*')
|
39
|
+
search_me = ::File.expand_path(
|
40
|
+
::File.join(::File.dirname(fname), dir, '**', '*.rb'))
|
41
|
+
|
42
|
+
Dir.glob(search_me).sort.each {|rb| require rb}
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,287 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[.. spec_helper])
|
2
|
+
|
3
|
+
module CapTaffy
|
4
|
+
describe 'Db' do
|
5
|
+
context "when loaded" do
|
6
|
+
include CapistranoHelpers
|
7
|
+
include TaffyHelpers
|
8
|
+
|
9
|
+
before do
|
10
|
+
CapTaffy.send(:remove_const, "Db") rescue nil
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should load in capistrano configuration instance" do;
|
14
|
+
Capistrano::Configuration.instance.expects(:load)
|
15
|
+
|
16
|
+
load 'lib/cap-taffy/db.rb'
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should define :db namespace" do
|
20
|
+
Capistrano::Configuration.instance.expects(:namespace).with(:db)
|
21
|
+
|
22
|
+
load 'lib/cap-taffy/db.rb'
|
23
|
+
end
|
24
|
+
|
25
|
+
for_task :detect, :roles => :app, :in => :Db, :it => "should be defined" do
|
26
|
+
@mod.expects(:remote_database_url).returns("remote_db_url")
|
27
|
+
@mod.expects(:local_database_url).returns("local_db_url")
|
28
|
+
|
29
|
+
load 'lib/cap-taffy/db.rb'
|
30
|
+
|
31
|
+
@namespace.instance_variable_get(:@remote_database_url).should == "remote_db_url"
|
32
|
+
@namespace.instance_variable_get(:@local_database_url).should == "local_db_url"
|
33
|
+
end
|
34
|
+
|
35
|
+
def load_taffy_db # :nodoc:
|
36
|
+
with_logger do
|
37
|
+
load 'lib/cap-taffy/db.rb'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
for_task :push, :roles => :app, :in => :Db, :it => "should send taps client cmd_send" do
|
42
|
+
options = {:remote_database_url => "remote", :local_database_url => "local", :port => nil, :login => "a_user", :password => "a_pass"}
|
43
|
+
@namespace.expects(:detect)
|
44
|
+
namespace_with_variables(:taps_port => nil)
|
45
|
+
db_with_expected_options(options)
|
46
|
+
@mod.expects(:tmp_pass).with(@namespace.fetch(:user)).returns(options[:password])
|
47
|
+
@mod.expects(:run).with(@namespace, options).yields(taps_client_who(:expects, :cmd_send))
|
48
|
+
|
49
|
+
load_taffy_db
|
50
|
+
end
|
51
|
+
|
52
|
+
for_task :push, :roles => :app, :in => :Db, :it => "should use cli argument for port" do
|
53
|
+
options = {:remote_database_url => "remote", :local_database_url => "local", :port => 1234, :login => "a_user", :password => "a_pass"}
|
54
|
+
@namespace.expects(:detect)
|
55
|
+
namespace_with_variables(:taps_port => 1234)
|
56
|
+
db_with_expected_options(options)
|
57
|
+
@mod.expects(:tmp_pass).with(@namespace.fetch(:user)).returns(options[:password])
|
58
|
+
@mod.expects(:run).with(@namespace, options)
|
59
|
+
|
60
|
+
load_taffy_db
|
61
|
+
end
|
62
|
+
|
63
|
+
for_task :push, :roles => :app, :in => :Db, :it => "should force 127.0.0.1 (local) for ssh local forwarding" do
|
64
|
+
options = {:remote_database_url => "remote", :local_database_url => "local", :port => 1234, :login => "a_user", :password => "a_pass"}
|
65
|
+
@namespace.expects(:detect)
|
66
|
+
namespace_with_variables(:taps_port => 1234, :local => true)
|
67
|
+
db_with_expected_options(options)
|
68
|
+
@mod.expects(:tmp_pass).with(@namespace.fetch(:user)).returns(options[:password])
|
69
|
+
@mod.expects(:run).with(@namespace, options.merge(:local => true))
|
70
|
+
|
71
|
+
load_taffy_db
|
72
|
+
end
|
73
|
+
|
74
|
+
for_task :pull, :roles => :app, :in => :Db, :it => "should send taps client cmd_receive" do
|
75
|
+
options = {:remote_database_url => "remote", :local_database_url => "local", :port => nil, :login => "a_user", :password => "a_pass"}
|
76
|
+
@namespace.expects(:detect)
|
77
|
+
namespace_with_variables(:taps_port => nil)
|
78
|
+
db_with_expected_options(options)
|
79
|
+
@mod.expects(:tmp_pass).with(@namespace.fetch(:user)).returns(options[:password])
|
80
|
+
@mod.expects(:run).with(@namespace, options).yields(taps_client_who(:expects, :cmd_receive))
|
81
|
+
|
82
|
+
load_taffy_db
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "after capistrano" do
|
87
|
+
include CapistranoHelpers
|
88
|
+
include TaffyHelpers
|
89
|
+
|
90
|
+
before do
|
91
|
+
Capistrano::Configuration.instance.expects(:namespace).with(:db)
|
92
|
+
CapTaffy.send(:remove_const, "Db") rescue nil
|
93
|
+
load 'lib/cap-taffy/db.rb'
|
94
|
+
|
95
|
+
@conf = {"test"=>
|
96
|
+
{"reconnect"=>false,
|
97
|
+
"encoding"=>"utf8",
|
98
|
+
"username"=>"root",
|
99
|
+
"adapter"=>"postgresql",
|
100
|
+
"database"=>"test_test",
|
101
|
+
"pool"=>5,
|
102
|
+
"password"=>"root",
|
103
|
+
"host"=>"localhost"}}
|
104
|
+
|
105
|
+
@options = {:remote_database_url => "remote", :local_database_url => "local", :port => nil, :login => "a_user", :password => "a_pass"}
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should detect local database url" do
|
109
|
+
File.expects(:exists?).returns(true)
|
110
|
+
File.expects(:read).returns(mock())
|
111
|
+
YAML.expects(:load).returns(@conf)
|
112
|
+
env = 'test'
|
113
|
+
Parse.expects(:database_url).with(@conf, env)
|
114
|
+
|
115
|
+
Db.local_database_url(env)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should detect remote database url" do
|
119
|
+
instance = mock()
|
120
|
+
instance.stubs(:current_path).returns("/home/user/public_html/current")
|
121
|
+
instance.expects(:capture).with("cat /home/user/public_html/current/config/database.yml")
|
122
|
+
YAML.expects(:load).returns(@conf)
|
123
|
+
env = 'test'
|
124
|
+
Parse.expects(:database_url).with(@conf, env)
|
125
|
+
|
126
|
+
Db.remote_database_url(instance, env)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should create temporary password from time and user" do
|
130
|
+
Time.stubs(:now).returns("asdfasdfasdf")
|
131
|
+
user = "me"
|
132
|
+
tmp_pass = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{user}--")
|
133
|
+
|
134
|
+
Db.tmp_pass(user).should == tmp_pass
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should expose a basic taps client" do
|
138
|
+
client = mock()
|
139
|
+
local_database_url = mock()
|
140
|
+
remote_url = mock()
|
141
|
+
Taps::Config.expects(:chunksize=).with(1000)
|
142
|
+
Taps::Config.expects(:database_url=).with(local_database_url)
|
143
|
+
Taps::Config.expects(:remote_url=).with(remote_url)
|
144
|
+
Taps::Config.expects(:verify_database_url)
|
145
|
+
client.expects(:do_something)
|
146
|
+
|
147
|
+
Taps::ClientSession.expects(:quickstart).yields(client)
|
148
|
+
|
149
|
+
Db.taps_client(local_database_url, remote_url) do |client|
|
150
|
+
client.do_something
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should define default port" do
|
155
|
+
Db.default_server_port.should == 5000
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should build server command" do
|
159
|
+
remote_database_url, login, password, port = @options[:remote_database_url], @options[:login], @options[:password], @options[:port]
|
160
|
+
|
161
|
+
Db.server_command(@options).should == "taps server #{remote_database_url} #{login} #{password}"
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should build server command with port" do
|
165
|
+
@options[:port] = 1234
|
166
|
+
remote_database_url, login, password, port = @options[:remote_database_url], @options[:login], @options[:password], @options[:port]
|
167
|
+
|
168
|
+
Db.server_command(@options).should == "taps server #{remote_database_url} #{login} #{password} --port=#{port}"
|
169
|
+
end
|
170
|
+
|
171
|
+
it "should build server command without port if same as default port" do
|
172
|
+
@options[:port] = 5000
|
173
|
+
remote_database_url, login, password = @options[:remote_database_url], @options[:login], @options[:password]
|
174
|
+
|
175
|
+
Db.server_command(@options).should == "taps server #{remote_database_url} #{login} #{password}"
|
176
|
+
end
|
177
|
+
|
178
|
+
def parser_expects_uri_hash_to_url_with(login, password, host)
|
179
|
+
parser = mock()
|
180
|
+
Parse.expects(:new).at_least_once.returns(parser)
|
181
|
+
parser.expects(:uri_hash_to_url).
|
182
|
+
with('username' => login, 'password' => password, 'host' => host, 'scheme' => 'http', 'path' => '')
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should build remote url (with some help)" do
|
186
|
+
@options[:host] = "127.0.0.1"
|
187
|
+
parser_expects_uri_hash_to_url_with(@options[:login], @options[:password], "#{@options[:host]}:#{Db.default_server_port}").returns("remote_url/")
|
188
|
+
|
189
|
+
Db.remote_url(@options)
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should build remote url with different port" do
|
193
|
+
@options[:host] = "127.0.0.1"
|
194
|
+
@options[:port] = 1234
|
195
|
+
parser_expects_uri_hash_to_url_with(@options[:login], @options[:password], "#{@options[:host]}:#{@options[:port]}").returns("remote_url")
|
196
|
+
|
197
|
+
Db.remote_url(@options)
|
198
|
+
end
|
199
|
+
|
200
|
+
it "should remove trailing slash from remote url" do
|
201
|
+
@options[:host] = "127.0.0.1"
|
202
|
+
parser_expects_uri_hash_to_url_with(@options[:login], @options[:password], "#{@options[:host]}:#{Db.default_server_port}").returns("remote_url/")
|
203
|
+
|
204
|
+
Db.remote_url(@options).should == "remote_url"
|
205
|
+
end
|
206
|
+
|
207
|
+
running_db_it "should run with capistrano" do
|
208
|
+
capistrano_run_with(Db.server_command(@options))
|
209
|
+
|
210
|
+
Db.run(@capistrano, @options)
|
211
|
+
end
|
212
|
+
|
213
|
+
running_db_it "should do something to taps client" do
|
214
|
+
channel, stream, data = simulating_run_loop_with :data => ">> Listening on 0.0.0.0:5000, CTRL+C to stop\r\n" do
|
215
|
+
capistrano_run_with(Db.server_command(@options))
|
216
|
+
end
|
217
|
+
channel.expects(:close)
|
218
|
+
|
219
|
+
Db.expects(:remote_url).with(@options.merge(:host => channel[:host], :port => 5000)).returns("remote_url")
|
220
|
+
Db.expects(:taps_client).with("local", "remote_url").yields(taps_client_who(:expects, :do_something))
|
221
|
+
Db.run(@capistrano, @options) do |client|
|
222
|
+
client.do_something
|
223
|
+
end
|
224
|
+
|
225
|
+
channel[:status].should == 0
|
226
|
+
end
|
227
|
+
|
228
|
+
running_db_it "should run taffy on different port" do
|
229
|
+
@options[:port] = 1234
|
230
|
+
|
231
|
+
channel, stream, data = simulating_run_loop_with :data => ">> Listening on 0.0.0.0:1234, CTRL+C to stop\r\n" do
|
232
|
+
capistrano_run_with(Db.server_command(@options))
|
233
|
+
end
|
234
|
+
channel.expects(:close)
|
235
|
+
Db.expects(:remote_url).with(@options.merge(:host => channel[:host])).returns("remote_url")
|
236
|
+
Db.expects(:taps_client).with("local", "remote_url").yields(taps_client_who(:expects, :do_something))
|
237
|
+
|
238
|
+
Db.run(@capistrano, @options) do |client|
|
239
|
+
client.do_something
|
240
|
+
end
|
241
|
+
|
242
|
+
channel[:status].should == 0
|
243
|
+
end
|
244
|
+
|
245
|
+
running_db_it "should not do anything until taps sinatra server is running" do
|
246
|
+
simulating_run_loop_with :data => "asdfasdf" do
|
247
|
+
capistrano_run_with(Db.server_command(@options))
|
248
|
+
end
|
249
|
+
|
250
|
+
client = mock()
|
251
|
+
client.expects(:do_something).never
|
252
|
+
|
253
|
+
Db.run(@capistrano, @options) do |client|
|
254
|
+
client.do_something
|
255
|
+
end
|
256
|
+
|
257
|
+
channel, stream, data = simulating_run_loop_with :data => ">> Listening on 0.0.0.0:5000, CTRL+C to stop\r\n" do
|
258
|
+
capistrano_run_with(Db.server_command(@options))
|
259
|
+
end
|
260
|
+
channel.expects(:close)
|
261
|
+
Db.expects(:remote_url).with(@options.merge(:host => channel[:host], :port => 5000)).returns("remote_url")
|
262
|
+
Db.expects(:taps_client).with("local", "remote_url").yields(taps_client_who(:expects, :do_something))
|
263
|
+
|
264
|
+
Db.run(@capistrano, @options) do |client|
|
265
|
+
client.do_something
|
266
|
+
end
|
267
|
+
|
268
|
+
channel[:status].should == 0
|
269
|
+
end
|
270
|
+
|
271
|
+
running_db_it "should force 127.0.0.1 (local) for remote url" do
|
272
|
+
channel, stream, data = simulating_run_loop_with :data => ">> Listening on 0.0.0.0:5000, CTRL+C to stop\r\n" do
|
273
|
+
capistrano_run_with(Db.server_command(@options))
|
274
|
+
end
|
275
|
+
channel.expects(:close)
|
276
|
+
|
277
|
+
Db.expects(:remote_url).with(@options.merge(:host => '127.0.0.1', :port => 5000)).returns("remote_url")
|
278
|
+
Db.expects(:taps_client).with("local", "remote_url").yields(taps_client_who(:expects, :do_something))
|
279
|
+
Db.run(@capistrano, @options.merge(:local => true)) do |client|
|
280
|
+
client.do_something
|
281
|
+
end
|
282
|
+
|
283
|
+
channel[:status].should == 0
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[.. spec_helper])
|
2
|
+
require 'lib/cap-taffy/parse'
|
3
|
+
|
4
|
+
module CapTaffy
|
5
|
+
describe Parse do
|
6
|
+
before do
|
7
|
+
@database_config = {"production"=>
|
8
|
+
{"reconnect"=>false,
|
9
|
+
"encoding"=>"utf8",
|
10
|
+
"username"=>"root",
|
11
|
+
"adapter"=>"mysql",
|
12
|
+
"database"=>"test_production",
|
13
|
+
"pool"=>5,
|
14
|
+
"password"=>"root",
|
15
|
+
"host"=>"localhost"},
|
16
|
+
"development"=>
|
17
|
+
{"username"=>"root",
|
18
|
+
"adapter"=>"sqlite3",
|
19
|
+
"database"=>"test_development",
|
20
|
+
"password"=>"root"},
|
21
|
+
"test"=>
|
22
|
+
{"reconnect"=>false,
|
23
|
+
"encoding"=>"utf8",
|
24
|
+
"username"=>"root",
|
25
|
+
"adapter"=>"postgresql",
|
26
|
+
"database"=>"test_test",
|
27
|
+
"pool"=>5,
|
28
|
+
"password"=>"root",
|
29
|
+
"host"=>"localhost"}}
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should do pass-through for escape" do
|
33
|
+
Parse.new.escape("something").should == "something"
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should parse database config for sqlite" do
|
37
|
+
Parse.expects(:conf_to_uri_hash).never
|
38
|
+
Parse.database_url(@database_config, 'development').should == "sqlite://test_development"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should parse database config for postgresql" do
|
42
|
+
Parse.database_url(@database_config, 'test').should == "postgres://root:root@localhost/test_test?encoding=utf8"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should parse database config for mysql" do
|
46
|
+
Parse.database_url(@database_config, 'production').should == "mysql://root:root@localhost/test_production?encoding=utf8"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should raise invalid conf if so" do
|
50
|
+
lambda { Parse.database_url(nil, nil) }.should raise_error(Parse::Invalid)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should raise invalid conf if so" do
|
54
|
+
lambda { Parse.database_url(@database_config, nil) }.should raise_error(Parse::Invalid)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[.. spec_helper])
|
2
|
+
|
3
|
+
module CapTaffy
|
4
|
+
describe 'SSH' do
|
5
|
+
include CapistranoHelpers
|
6
|
+
|
7
|
+
before do
|
8
|
+
CapTaffy.send(:remove_const, "SSH") rescue nil
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should load in capistrano configuration instance" do;
|
12
|
+
Capistrano::Configuration.instance.expects(:load)
|
13
|
+
|
14
|
+
load 'lib/cap-taffy/ssh.rb'
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should define :db namespace" do
|
18
|
+
Capistrano::Configuration.instance.expects(:namespace).with(:ssh)
|
19
|
+
|
20
|
+
load 'lib/cap-taffy/ssh.rb'
|
21
|
+
end
|
22
|
+
|
23
|
+
def simulating_line(line, &blk)
|
24
|
+
blk.call.then.yields(line)
|
25
|
+
line
|
26
|
+
end
|
27
|
+
|
28
|
+
for_task :authorize, :in => :SSH, :it => "should authorize on each server" do
|
29
|
+
public_key = "ssh-key2\n"
|
30
|
+
File.expects(:read).with(File.expand_path(File.join(%w[~/ .ssh id_rsa.pub]))).returns(public_key)
|
31
|
+
|
32
|
+
run_with(@namespace, anything) # Don't rly wna test bash scripts
|
33
|
+
|
34
|
+
load 'lib/cap-taffy/ssh.rb'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib cap-taffy]))
|
2
|
+
|
3
|
+
module Capistrano # :nodoc:
|
4
|
+
end
|
5
|
+
|
6
|
+
Spec::Runner.configure do |config|
|
7
|
+
# == Mock Framework
|
8
|
+
#
|
9
|
+
# RSpec uses it's own mocking framework by default. If you prefer to
|
10
|
+
# use mocha, flexmock or RR, uncomment the appropriate line:
|
11
|
+
#
|
12
|
+
config.mock_with :mocha
|
13
|
+
# config.mock_with :flexmock
|
14
|
+
# config.mock_with :rr
|
15
|
+
end
|
16
|
+
|
17
|
+
Capistrano.send(:remove_const, "Configuration") rescue nil
|
18
|
+
Capistrano.const_set("Configuration", Class.new)
|
19
|
+
Capistrano::Configuration.class_eval do
|
20
|
+
def self.instance
|
21
|
+
@instance ||= Capistrano::Configuration.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def load(*args, &block)
|
25
|
+
instance_eval(&block) if block_given?
|
26
|
+
end
|
27
|
+
|
28
|
+
def namespaces
|
29
|
+
@namespaces ||= {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def namespace(name, &block)
|
33
|
+
namespaces[name].instance_eval(&block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class String # :nodoc:
|
38
|
+
def demodulize
|
39
|
+
gsub(/^.*::/, '')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module CapistranoHelpers
|
44
|
+
def self.included(base) # :nodoc:
|
45
|
+
base.extend CapistranoHelpers::ClassMethods
|
46
|
+
end
|
47
|
+
|
48
|
+
# Stubs the Capistrano Logger and yields to the block
|
49
|
+
def with_logger(&blk) # :yields:
|
50
|
+
logger_class = Class.new
|
51
|
+
logger = mock()
|
52
|
+
logger.stub_everything
|
53
|
+
logger_class.stubs(:new).returns(logger)
|
54
|
+
Capistrano.const_set("Logger", logger_class)
|
55
|
+
yield
|
56
|
+
ensure
|
57
|
+
Capistrano.send(:remove_const, "Logger") rescue nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# Helper for common operations in db tasks
|
61
|
+
def db_with_expected_options(options) # :nodoc:
|
62
|
+
@namespace.stubs(:fetch).with(:user).returns(options[:login])
|
63
|
+
@namespace.instance_variable_set(:@remote_database_url, options[:remote_database_url])
|
64
|
+
@namespace.instance_variable_set(:@local_database_url, options[:local_database_url])
|
65
|
+
end
|
66
|
+
|
67
|
+
# Stubs the variables hash used by Capistrano to accept command-line parameters
|
68
|
+
def namespace_with_variables(variables) # :nodoc:
|
69
|
+
@namespace.stubs(:variables).returns(variables)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Creates an expectation for the Capistrano namespace/task <tt>instance</tt> for the <tt>:run</tt> action with <tt>*args</tt>.
|
73
|
+
def run_with(instance, *args)
|
74
|
+
instance.expects(:run).with(*args)
|
75
|
+
end
|
76
|
+
|
77
|
+
# The Capistrano <tt>:run</tt> action loops with <tt>channel</tt>, <tt>stream</tt>, and <tt>data</tt> until the channel is closed.
|
78
|
+
#
|
79
|
+
# Passing in a <tt>:run</tt> expectation, modifies the expectation such that each subsequent invocation ("loop") yields <tt>channel</tt>, <tt>stream</tt>, and <tt>data</tt>
|
80
|
+
#
|
81
|
+
# ==== Parameters
|
82
|
+
#
|
83
|
+
# * <tt>:channel</tt> - A hash containing <tt>:host</tt>.
|
84
|
+
# * <tt>:stream</tt> - A stream object.
|
85
|
+
# * <tt>:data</tt> - A data object, usually a String.
|
86
|
+
def simulating_run_loop_with(options={}, &blk) # :yields:
|
87
|
+
channel = options[:channel] || {:host => "192.168.1.20"}
|
88
|
+
stream = options[:stream]
|
89
|
+
data = options[:data]
|
90
|
+
|
91
|
+
blk.call.then.yields(channel, stream, data)
|
92
|
+
[channel, stream, data]
|
93
|
+
end
|
94
|
+
|
95
|
+
module ClassMethods
|
96
|
+
# Used in specs to test Capistrano tasks.
|
97
|
+
#
|
98
|
+
#
|
99
|
+
# Code defined in the task will be executed automatically on load. (Note the <tt>yields</tt>, see Mocha#yields[http://mocha.rubyforge.org/classes/Mocha/Expectation.html#M000043])
|
100
|
+
#
|
101
|
+
# ==== Parameters
|
102
|
+
#
|
103
|
+
# * <tt>:it</tt> - The description for the current example group.
|
104
|
+
# * <tt>:in</tt> - Specifies the module under test (as well as the namespace the task is defined in). The namespace will be deduced with the downcase of <tt>:in</tt>
|
105
|
+
#
|
106
|
+
# ==== Examples
|
107
|
+
#
|
108
|
+
# for_task :detect, :roles => :app, :in => :Somewhere, :it => "should so something" do
|
109
|
+
# @namespace # refers to the current task under test (in Capistrano tasks are executed on Capistrano::Namespaces::Namespace instances)
|
110
|
+
# @mod # refers to module CapTaffy::Somewhere
|
111
|
+
# end
|
112
|
+
def for_task(task_name, options, &block)
|
113
|
+
description = options.delete(:it)
|
114
|
+
namespace = options.delete(:in)
|
115
|
+
|
116
|
+
context ":#{task_name.to_s} task" do
|
117
|
+
before do
|
118
|
+
@namespace = Capistrano::Configuration.instance.namespaces[namespace.to_s.downcase.to_sym] = mock()
|
119
|
+
@namespace.stubs(:desc)
|
120
|
+
@namespace.stubs(:task)
|
121
|
+
args = [task_name, options]
|
122
|
+
unless options.empty?
|
123
|
+
@namespace.expects(:task).with(*args).yields
|
124
|
+
else
|
125
|
+
@namespace.expects(:task).with(task_name).yields
|
126
|
+
end
|
127
|
+
|
128
|
+
@mod = Module.new # The module under test
|
129
|
+
CapTaffy.const_set(namespace, @mod)
|
130
|
+
end
|
131
|
+
|
132
|
+
it description, &block
|
133
|
+
|
134
|
+
after do
|
135
|
+
const_name = @mod.to_s.demodulize
|
136
|
+
CapTaffy.send(:remove_const, const_name)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
module TaffyHelpers
|
144
|
+
def self.included(base) # :nodoc:
|
145
|
+
base.extend TaffyHelpers::ClassMethods
|
146
|
+
end
|
147
|
+
|
148
|
+
# A simple helper for mocking a quick object
|
149
|
+
#
|
150
|
+
# Usage:
|
151
|
+
# taps_client_who(:expects, :do_something)
|
152
|
+
def taps_client_who(method_symbol, *args)
|
153
|
+
client = mock()
|
154
|
+
client.send(method_symbol, *args)
|
155
|
+
client
|
156
|
+
end
|
157
|
+
|
158
|
+
module ClassMethods
|
159
|
+
# A wrapper for running CapTaffy::Db::run
|
160
|
+
def running_db_it(message, &blk)
|
161
|
+
context "when running db" do
|
162
|
+
before do
|
163
|
+
@capistrano = mock()
|
164
|
+
end
|
165
|
+
|
166
|
+
# See CapistranoHelpers
|
167
|
+
def capistrano_run_with(*args)
|
168
|
+
run_with(@capistrano, *args)
|
169
|
+
end
|
170
|
+
|
171
|
+
it message, &blk
|
172
|
+
|
173
|
+
after do
|
174
|
+
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hsume2-cap-taffy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Henry Hsu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-17 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: heroku
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: taps
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: capistrano
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: bones
|
47
|
+
type: :development
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.5.1
|
54
|
+
version:
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mocha
|
57
|
+
type: :development
|
58
|
+
version_requirement:
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
description: Capistrano recipes for deploying databases and other common tasks.
|
66
|
+
email: henry@qlane.com
|
67
|
+
executables: []
|
68
|
+
|
69
|
+
extensions: []
|
70
|
+
|
71
|
+
extra_rdoc_files:
|
72
|
+
- History.txt
|
73
|
+
files:
|
74
|
+
- History.txt
|
75
|
+
- README.md
|
76
|
+
- Rakefile
|
77
|
+
- cap-taffy.gemspec
|
78
|
+
- lib/cap-taffy.rb
|
79
|
+
- lib/cap-taffy/db.rb
|
80
|
+
- lib/cap-taffy/parse.rb
|
81
|
+
- lib/cap-taffy/ssh.rb
|
82
|
+
- spec/cap-taffy/db_spec.rb
|
83
|
+
- spec/cap-taffy/parse_spec.rb
|
84
|
+
- spec/cap-taffy/ssh_spec.rb
|
85
|
+
- spec/cap-taffy_spec.rb
|
86
|
+
- spec/spec.opts
|
87
|
+
- spec/spec_helper.rb
|
88
|
+
has_rdoc: false
|
89
|
+
homepage: http://by.qlane.com
|
90
|
+
licenses:
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options:
|
93
|
+
- --main
|
94
|
+
- README.md
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: "0"
|
102
|
+
version:
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: "0"
|
108
|
+
version:
|
109
|
+
requirements: []
|
110
|
+
|
111
|
+
rubyforge_project: cap-taffy
|
112
|
+
rubygems_version: 1.3.5
|
113
|
+
signing_key:
|
114
|
+
specification_version: 3
|
115
|
+
summary: Capistrano recipes for deploying databases (managing database.yml, importing/exporting/transfering databases, etc.)
|
116
|
+
test_files: []
|
117
|
+
|