dbcp 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dae551f41d7c976a088cafc8ebe340418b9e26ec
4
- data.tar.gz: 1210fe47e8461e62afac77a58c238c346020e313
3
+ metadata.gz: e37c2566e5ac25d166c8fd27df6209e2f83863f1
4
+ data.tar.gz: 0960077bc0488bd979f3b97b751a940b3e6f11c4
5
5
  SHA512:
6
- metadata.gz: 88e7b3c91a04ecf258f16cf848aff23a3925653b175cd35ccc0ff1fca586a6ed2b50a1325c463249c3468c0d1f2dc48a0d17638ce67e85742272cbd17dba82cd
7
- data.tar.gz: d6adbc1c6773aaf9a6a1748e94c3683ba4f91772022f66172bba177df6d8135959a5406c9f420d7e595dfd79216fe145f95f11129cea6d610f0cb459edd28055
6
+ metadata.gz: 0d8b2f092e45700399fbfda1e91cdb0c80205ecf9670b07c5b5996d153ddf9570df48c57278a8427e7ec32350d11148356864f3a0c427d74dc462a45b0747ff0
7
+ data.tar.gz: fdde59cd6bd19e060aefd6a1c5999087ebeab62aa2c08c5f4135fadb320979bffb0e8b514256258fc65767646a990b16ec498bbd686aee5741af50b491d70bec
data/README.md CHANGED
@@ -5,7 +5,9 @@
5
5
 
6
6
  Copy Postgres or MySQL databases between application environments.
7
7
 
8
- Setting an employee up to work on a web application for the first time is time consuming. Tools like [Vagrant](http://www.vagrantup.com) have made it easy to get your environment setup, but you still need to get your relational database setup. In rails you can load your `db/schema.rb` and hope that `db/seeds.rb` is well curated, but seldom has enough to let a developer hit the ground running. Working with your production database while developing is extremely convenient. The [parity](http://12factor.net/dev-prod-parity) helps preview database performance. It also makes investigating data-specific bugs much easier. The goal of `dbcp` is to make copying databases between development, staging, and production environments as easy as copying a file on your local disk.
8
+ Setting an employee up to work on a web application for the first time is time consuming. Tools like [Vagrant](http://www.vagrantup.com) have made it easy to get your environment setup, but you still need to get your relational database setup. In rails you can load your `db/schema.rb` and hope that `db/seeds.rb` is well curated, but seldom has enough to let a developer hit the ground running. Working with your production database while developing is extremely convenient. The [parity](http://12factor.net/dev-prod-parity) helps preview database performance. It also makes investigating data-specific bugs much easier.
9
+
10
+ The goal of `dbcp` is to make copying databases between development, staging, and production environments as easy as copying a file on your local disk. It's an adapter for platform-specific utilities like `pg_dump` and `mysqldump`, simplifies lookup of credentials using storage mechanisms you're already using, and handles transfer of dump files between hosts.
9
11
 
10
12
  ## A word of caution
11
13
 
@@ -37,30 +39,59 @@ Environment credentials can be defined in the following providers:
37
39
 
38
40
  Rails defines credentials for its database environments in a file at [`config/database.yml`](https://github.com/rails/rails/blob/master/guides/code/getting_started/config/database.yml). By default this file is generated with only development and test environments, but any additional environments added will be leveraged by `dbcp`. Although this is a rails convention, `dbcp` parses this file outside of any framework, so it will work even if you're using this convention in another framework.
39
41
 
40
- The database export or import can be executed on a remote host over ssh and then copied between environment hosts if you specify the remote host via an `ssh_uri` entry in the database.yml. This is helpful if the database host only allows connections from specific servers.
42
+ The database export or import can be executed on a remote host over ssh and then copied between environment hosts if you specify the remote host via an `ssh_uri` entry in the database.yml. This is helpful if the database host only allows connections from specific servers. If your `ssh_uri` optionally includes a path to your application root on the remote server, dbcp will load the database credentials from the remote server's config/database.yml.
41
43
 
42
44
  Example config/database.yml:
43
45
 
44
46
  ```yaml
47
+ # Local database
48
+ development:
49
+ adapter: postgresql
50
+ encoding: unicode
51
+ pool: 5
52
+ database: development_database
53
+ username: development_username
54
+ password: development_password
55
+
56
+ # Remote database, credentials provided locally, executed from remote host over ssh
45
57
  staging:
46
58
  adapter: postgresql
47
59
  database: staging_database
48
60
  username: staging_username
49
61
  password: staging_password
50
- ssh_uri: ssh://deploy@staging.example.com/www/staging.example.com/current
62
+ ssh_uri: ssh://deploy@staging.example.com
63
+
64
+ # Remote database, credentials fetched over ssh, executed from remote host over ssh
65
+ production:
66
+ ssh_uri: ssh://deploy@production.example.com/www/production.example.com/current
51
67
  ```
52
68
 
69
+ $ dbcp staging development
70
+
71
+ ### URI
72
+
73
+ You can use a database URI in place of an environment name as follows:
74
+
75
+ $ dbcp postgres://my_username:my_pass@db.example.com/my_database development
76
+
53
77
  ## Roadmap
54
78
 
55
79
  The following features are pending:
56
80
 
57
81
  Providers:
58
82
 
59
- - URL passed in as environment
60
- - capistrano task
61
- - heroku, inferred from git remotes
83
+ - Capistrano task
84
+ - Heroku, environment name inferred from git remotes
62
85
 
63
86
  Features:
64
87
 
65
- - Reading configuration from a remote config/database.yml
66
88
  - Definable per-tool specific options, e.g. to allow pg_dump to provide a table exclusion list
89
+ - URI Provider: specify an remote ssh execution host, perhaps using '@@' as a URI separator?
90
+
91
+ Refactors:
92
+
93
+ - Handle pg_restore warnings
94
+ - Better logging
95
+ - Better help
96
+
97
+ [Open an issue](https://github.com/gabetax/dbcp/issues) if there's something else you'd like to see supported.
data/lib/dbcp/database.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  module Dbcp
2
2
  class Database
3
+ class BlankDatabaseDefinition < StandardError; end
3
4
  class UnsupportedDatabaseAdapter < StandardError; end
4
5
 
5
6
  class << self
@@ -13,6 +14,8 @@ module Dbcp
13
14
  MysqlDatabase
14
15
  when /postgres/
15
16
  PostgresDatabase
17
+ when nil, ''
18
+ raise BlankDatabaseDefinition.new("No database adapter was provided.")
16
19
  else
17
20
  raise UnsupportedDatabaseAdapter.new("Unsupported database adapter: #{adapter}")
18
21
  end
@@ -24,6 +27,7 @@ module Dbcp
24
27
  attribute :adapter
25
28
  attribute :database
26
29
  attribute :host, String, default: 'localhost'
30
+ attribute :port, Fixnum
27
31
  attribute :socket
28
32
  attribute :username
29
33
  attribute :password
@@ -1,21 +1,25 @@
1
1
  module Dbcp
2
2
  class MysqlDatabase < Database
3
3
  def export_command(snapshot_file)
4
- %W[mysqldump #{socket_or_host} --user=#{username} --password=#{password} --add-drop-table --extended-insert --result-file=#{snapshot_file.path} #{database}].shelljoin
4
+ %W[mysqldump #{build_socket_or_host} #{build_port} --user=#{username} --password=#{password} --add-drop-table --extended-insert --result-file=#{snapshot_file.path} #{database}].reject(&:empty?).shelljoin
5
5
  end
6
6
 
7
7
  def import_command(snapshot_file)
8
- %W[mysql #{socket_or_host} --user=#{username} --password=#{password} #{database}].shelljoin + ' < ' + snapshot_file.path.shellescape
8
+ %W[mysql #{build_socket_or_host} #{build_port} --user=#{username} --password=#{password} #{database}].reject(&:empty?).shelljoin + ' < ' + snapshot_file.path.shellescape
9
9
  end
10
10
 
11
11
  private
12
12
 
13
- def socket_or_host
13
+ def build_socket_or_host
14
14
  if socket
15
15
  "--socket=#{socket}"
16
16
  else
17
17
  "--host=#{host}"
18
18
  end
19
19
  end
20
+
21
+ def build_port
22
+ "--port=#{port}" if host && port
23
+ end
20
24
  end
21
25
  end
@@ -1,11 +1,38 @@
1
1
  module Dbcp
2
2
  class PostgresDatabase < Database
3
3
  def export_command(snapshot_file)
4
- %W[export PGPASSWORD=#{password}].shelljoin + '; ' + %W[pg_dump --host #{host} --username #{username} --file #{snapshot_file.path} --format c #{database}].shelljoin
4
+ build_password + [
5
+ 'pg_dump',
6
+ '--host', host,
7
+ build_port,
8
+ '--username', username,
9
+ '--file', snapshot_file.path,
10
+ '--format', 'c',
11
+ database
12
+ ].flatten.compact.shelljoin
5
13
  end
6
14
 
7
15
  def import_command(snapshot_file)
8
- %W[export PGPASSWORD=#{password}].shelljoin + '; ' + %W[pg_restore --host #{host} --username #{username} --clean --dbname #{database} #{snapshot_file.path}].shelljoin
16
+ build_password + [
17
+ 'pg_restore',
18
+ '--host', host,
19
+ build_port,
20
+ '--username', username,
21
+ '--dbname', database,
22
+ '--clean',
23
+ snapshot_file.path
24
+ ].flatten.compact.shelljoin
9
25
  end
26
+
27
+ private
28
+
29
+ def build_password
30
+ %W[export PGPASSWORD=#{password}].shelljoin + '; '
31
+ end
32
+
33
+ def build_port
34
+ ['--port', port] if host && port
35
+ end
36
+
10
37
  end
11
38
  end
@@ -4,7 +4,8 @@ module Dbcp
4
4
 
5
5
  class Environment
6
6
  ENVIRONMENT_PROVIDERS = [
7
- DatabaseYamlEnvironmentProvider.new('config/database.yml')
7
+ DatabaseYamlEnvironmentProvider.new('config/database.yml'),
8
+ UriEnvironmentProvider.new
8
9
  ]
9
10
 
10
11
  class << self
@@ -27,11 +27,28 @@ module Dbcp
27
27
  end
28
28
 
29
29
  def build_environment(environment_name, environment_hash)
30
+ execution_host = ExecutionHost.build(environment_hash)
31
+
32
+ begin
33
+ database = Database.build(environment_hash)
34
+ rescue Database::BlankDatabaseDefinition => e
35
+ if execution_host.remote?
36
+ database = Database.build fetch_remote_environment_hash(execution_host)[environment_name]
37
+ else
38
+ raise e
39
+ end
40
+ end
41
+
30
42
  Environment.new({
31
43
  environment_name: environment_name,
32
- database: Database.build(environment_hash),
33
- execution_host: ExecutionHost.build(environment_hash)
44
+ database: database,
45
+ execution_host: execution_host
34
46
  })
35
47
  end
48
+
49
+ def fetch_remote_environment_hash(execution_host)
50
+ YAML.load execution_host.download "#{execution_host.path}/#{@database_yaml_path}"
51
+ end
52
+
36
53
  end
37
54
  end
@@ -0,0 +1,29 @@
1
+ require 'uri'
2
+
3
+ module Dbcp
4
+ class UriEnvironmentProvider
5
+ def find(environment_name)
6
+ uri = URI.parse(environment_name)
7
+ return nil unless uri.scheme
8
+
9
+ build_environment environment_name, uri
10
+ end
11
+
12
+ private
13
+
14
+ def build_environment(environment_name, uri)
15
+ Environment.new({
16
+ environment_name: environment_name,
17
+ database: Database.build({
18
+ 'adapter' => uri.scheme,
19
+ 'username' => uri.user,
20
+ 'password' => uri.password,
21
+ 'host' => uri.host,
22
+ 'port' => uri.port,
23
+ 'database' => uri.path[1..-1] # Trim leading '/'
24
+ }),
25
+ execution_host: LocalExecutionHost.new
26
+ })
27
+ end
28
+ end
29
+ end
@@ -65,7 +65,8 @@ module Dbcp
65
65
  raise ExecutionError.new "Execution failed with exit code #{$?.exitstatus}. Command was: #{command}" if exitstatus > 0
66
66
  end
67
67
 
68
- def download(source_path, destination_path)
68
+ # Omitting destination_path will return file contents as a string
69
+ def download(source_path, destination_path = nil)
69
70
  Net::SFTP.start host, username do |ssh|
70
71
  return ssh.download! source_path, destination_path
71
72
  end
data/lib/dbcp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Dbcp
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/dbcp.rb CHANGED
@@ -1,11 +1,11 @@
1
- require 'pry'
2
-
3
1
  require 'logger'
2
+ require 'shellwords'
4
3
  require 'virtus'
5
4
  require 'dbcp/cli'
6
5
  require 'dbcp/database'
7
6
  require 'dbcp/databases/mysql_database'
8
7
  require 'dbcp/databases/postgres_database'
8
+ require 'dbcp/environment_providers/uri_environment_provider'
9
9
  require 'dbcp/environment_providers/database_yaml_environment_provider'
10
10
  require 'dbcp/execution_host'
11
11
  require 'dbcp/execution_hosts/local_execution_host'
@@ -3,6 +3,8 @@ development:
3
3
  encoding: unicode
4
4
  database: dev_database
5
5
  pool: 5
6
+ host: db.example.com
7
+ port: 6543
6
8
  username: dev_username
7
9
  password: dev_password
8
10
 
@@ -13,4 +15,7 @@ staging:
13
15
  pool: 5
14
16
  username: staging_username
15
17
  password: staging_password
18
+ ssh_uri: ssh://deploy@staging.example.com
19
+
20
+ staging_ssh_only:
16
21
  ssh_uri: ssh://deploy@staging.example.com/www/staging.example.com/current
@@ -0,0 +1,7 @@
1
+ staging_ssh_only:
2
+ adapter: postgresql
3
+ encoding: unicode
4
+ database: remote_staging_database
5
+ pool: 5
6
+ username: remote_staging_username
7
+ password: remote_staging_password
@@ -4,7 +4,8 @@ describe Dbcp::Cli do
4
4
  subject { Dbcp::Cli.new silent_stdout }
5
5
  let(:silent_stdout) { '/dev/null' }
6
6
 
7
- before { Dir.chdir lib = File.expand_path('../../../fixtures', __FILE__) }
7
+ extend ExecuteInDirectory
8
+ execute_in_directory(File.expand_path('../../../fixtures', __FILE__))
8
9
 
9
10
  describe "#start" do
10
11
  context "success" do
@@ -2,12 +2,16 @@ require 'spec_helper'
2
2
 
3
3
  describe Dbcp::Database do
4
4
  describe ".build" do
5
- context "valid type" do
5
+ context "supported type" do
6
6
  specify { expect(Dbcp::Database.build 'adapter' => 'postgresql').to be_a(Dbcp::PostgresDatabase) }
7
7
  end
8
8
 
9
- context "invalid type" do
10
- specify { expect { Dbcp::Database.build 'adapter' => 'invalid'}.to raise_error(Dbcp::Database::UnsupportedDatabaseAdapter) }
9
+ context "no type" do
10
+ specify { expect { Dbcp::Database.build 'adapter' => nil}.to raise_error(Dbcp::Database::BlankDatabaseDefinition) }
11
+ end
12
+
13
+ context "unsupported type" do
14
+ specify { expect { Dbcp::Database.build 'adapter' => 'unsupported'}.to raise_error(Dbcp::Database::UnsupportedDatabaseAdapter) }
11
15
  end
12
16
 
13
17
  end
@@ -2,7 +2,10 @@ require 'spec_helper'
2
2
 
3
3
  describe Dbcp::DatabaseYamlEnvironmentProvider do
4
4
  subject { Dbcp::DatabaseYamlEnvironmentProvider.new path }
5
- let(:path) { File.expand_path('../../../../fixtures/config/database.yml', __FILE__) }
5
+ let(:path) { 'config/database.yml' }
6
+
7
+ extend ExecuteInDirectory
8
+ execute_in_directory(File.expand_path('../../../../fixtures', __FILE__))
6
9
 
7
10
  describe "#find" do
8
11
  context "when environment exists" do
@@ -12,6 +15,8 @@ describe Dbcp::DatabaseYamlEnvironmentProvider do
12
15
  expect(environment.database).to be_a Dbcp::Database
13
16
  expect(environment.environment_name).to eq 'development'
14
17
  expect(environment.database.adapter).to eq 'postgresql'
18
+ expect(environment.database.host).to eq 'db.example.com'
19
+ expect(environment.database.port).to eq 6543
15
20
  expect(environment.database.database).to eq 'dev_database'
16
21
  expect(environment.database.username).to eq 'dev_username'
17
22
  expect(environment.database.password).to eq 'dev_password'
@@ -30,10 +35,25 @@ describe Dbcp::DatabaseYamlEnvironmentProvider do
30
35
  expect(environment.execution_host).to be_a Dbcp::SshExecutionHost
31
36
  end
32
37
  end
38
+
39
+ context "without database definition, but with ssh_uri" do
40
+ let(:remote_yaml) { File.read remote_yaml_path }
41
+ let(:remote_yaml_path) { File.expand_path('../../../../fixtures/config/remote_database.yml', __FILE__) }
42
+ it "fetches database definition from database.yml on remote host" do
43
+ allow_any_instance_of(Dbcp::SshExecutionHost).to receive(:download).with('/www/staging.example.com/current/config/database.yml') { remote_yaml }
44
+ environment = subject.find 'staging_ssh_only'
45
+ expect(environment.database).to be_a Dbcp::PostgresDatabase
46
+ expect(environment.database.database).to eq 'remote_staging_database'
47
+ expect(environment.database.username).to eq 'remote_staging_username'
48
+ expect(environment.database.password).to eq 'remote_staging_password'
49
+ end
50
+ end
33
51
  end
52
+
34
53
  context "when environment doesn't exist" do
35
54
  specify { expect(subject.find 'does-not-exist').to be_nil }
36
55
  end
56
+
37
57
  context "when file doesn't exist" do
38
58
  let(:path) { File.expand_path('../../../../fixtures/config/database-does-not-exist.yml', __FILE__) }
39
59
  specify { expect(subject.find 'development').to be_nil }
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dbcp::UriEnvironmentProvider do
4
+ describe "#find" do
5
+ context "when a URI" do
6
+ let(:uri) { 'postgres://my_username:my_password@db.example.com:5432/my_database' }
7
+ it "returns an environment" do
8
+ environment = subject.find(uri)
9
+ expect(environment).to be_a Dbcp::Environment
10
+ expect(environment.environment_name).to eq uri
11
+ expect(environment.database).to be_a Dbcp::PostgresDatabase
12
+ expect(environment.database.username).to eq 'my_username'
13
+ expect(environment.database.password).to eq 'my_password'
14
+ expect(environment.database.host).to eq 'db.example.com'
15
+ expect(environment.database.database).to eq 'my_database'
16
+ end
17
+ end
18
+
19
+ context "when not a valid URI" do
20
+ let(:uri) { 'development'}
21
+ specify { expect(subject.find(uri)).to be_nil }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ module ExecuteInDirectory
2
+ def execute_in_directory(path)
3
+ around(:each) do |example|
4
+ old = Dir.pwd
5
+ Dir.chdir path
6
+ example.run
7
+ Dir.chdir old
8
+ end
9
+ end
10
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabe Martin-Dempesy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-04-07 00:00:00.000000000 Z
11
+ date: 2014-04-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: virtus
@@ -146,23 +146,27 @@ files:
146
146
  - lib/dbcp/databases/postgres_database.rb
147
147
  - lib/dbcp/environment.rb
148
148
  - lib/dbcp/environment_providers/database_yaml_environment_provider.rb
149
+ - lib/dbcp/environment_providers/uri_environment_provider.rb
149
150
  - lib/dbcp/execution_host.rb
150
151
  - lib/dbcp/execution_hosts/local_execution_host.rb
151
152
  - lib/dbcp/execution_hosts/ssh_execution_host.rb
152
153
  - lib/dbcp/version.rb
153
154
  - spec/fixtures/config/database.yml
155
+ - spec/fixtures/config/remote_database.yml
154
156
  - spec/lib/dbcp/cli_spec.rb
155
157
  - spec/lib/dbcp/database_snapshot_file_spec.rb
156
158
  - spec/lib/dbcp/database_spec.rb
157
159
  - spec/lib/dbcp/databases/mysql_database_spec.rb
158
160
  - spec/lib/dbcp/databases/postgres_database_spec.rb
159
161
  - spec/lib/dbcp/environment_providers/database_yaml_environment_provider_spec.rb
162
+ - spec/lib/dbcp/environment_providers/uri_environment_provider_spec.rb
160
163
  - spec/lib/dbcp/environment_spec.rb
161
164
  - spec/lib/dbcp/execution_host_spec.rb
162
165
  - spec/lib/dbcp/execution_hosts/local_execution_host_spec.rb
163
166
  - spec/lib/dbcp/execution_hosts/ssh_execution_host_spec.rb
164
167
  - spec/spec_helper.rb
165
168
  - spec/support/.keep
169
+ - spec/support/execute_in_directory.rb
166
170
  homepage: https://github.com/gabetax/dbcp
167
171
  licenses:
168
172
  - MIT
@@ -189,15 +193,18 @@ specification_version: 4
189
193
  summary: ''
190
194
  test_files:
191
195
  - spec/fixtures/config/database.yml
196
+ - spec/fixtures/config/remote_database.yml
192
197
  - spec/lib/dbcp/cli_spec.rb
193
198
  - spec/lib/dbcp/database_snapshot_file_spec.rb
194
199
  - spec/lib/dbcp/database_spec.rb
195
200
  - spec/lib/dbcp/databases/mysql_database_spec.rb
196
201
  - spec/lib/dbcp/databases/postgres_database_spec.rb
197
202
  - spec/lib/dbcp/environment_providers/database_yaml_environment_provider_spec.rb
203
+ - spec/lib/dbcp/environment_providers/uri_environment_provider_spec.rb
198
204
  - spec/lib/dbcp/environment_spec.rb
199
205
  - spec/lib/dbcp/execution_host_spec.rb
200
206
  - spec/lib/dbcp/execution_hosts/local_execution_host_spec.rb
201
207
  - spec/lib/dbcp/execution_hosts/ssh_execution_host_spec.rb
202
208
  - spec/spec_helper.rb
203
209
  - spec/support/.keep
210
+ - spec/support/execute_in_directory.rb