dbcp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dae551f41d7c976a088cafc8ebe340418b9e26ec
4
+ data.tar.gz: 1210fe47e8461e62afac77a58c238c346020e313
5
+ SHA512:
6
+ metadata.gz: 88e7b3c91a04ecf258f16cf848aff23a3925653b175cd35ccc0ff1fca586a6ed2b50a1325c463249c3468c0d1f2dc48a0d17638ce67e85742272cbd17dba82cd
7
+ data.tar.gz: d6adbc1c6773aaf9a6a1748e94c3683ba4f91772022f66172bba177df6d8135959a5406c9f420d7e595dfd79216fe145f95f11129cea6d610f0cb459edd28055
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0
5
+ - 2.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dbcp.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Gabe Martin-Dempesy
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # dbcp
2
+
3
+ [![Build Status](https://travis-ci.org/gabetax/dbcp.svg?branch=master)](https://travis-ci.org/gabetax/dbcp)
4
+ [![Code Climate](https://codeclimate.com/github/gabetax/dbcp.png)](https://codeclimate.com/github/gabetax/dbcp)
5
+
6
+ Copy Postgres or MySQL databases between application environments.
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.
9
+
10
+ ## A word of caution
11
+
12
+ Depending on your application, your production database may contain sensitive personal information like financial or health data. Give careful consideration to the risks of using production data on staging and development environments, and whether it's acceptable to use a tool like this in your application's workflow.
13
+
14
+ Treat your production database like a loaded gun. Consider employing some of these safe guards:
15
+
16
+ - Use a separate account that only has read access.
17
+ - Access a replication "follower" instead of the master.
18
+ - Access a completely separate database that is periodically populated from production, but is updated to use fake values for sensitive information.
19
+
20
+ ## Installation
21
+
22
+ `dbcp` is a stand-alone utility. To install, just run:
23
+
24
+ $ gem install dbcp
25
+
26
+ You should __not__ need to include dbcp in your `Gemfile`.
27
+
28
+ ## Usage
29
+
30
+ To copy the production database to the development environment, simply run:
31
+
32
+ $ dbcp production development
33
+
34
+ Environment credentials can be defined in the following providers:
35
+
36
+ ### config/database.yml
37
+
38
+ 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
+
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.
41
+
42
+ Example config/database.yml:
43
+
44
+ ```yaml
45
+ staging:
46
+ adapter: postgresql
47
+ database: staging_database
48
+ username: staging_username
49
+ password: staging_password
50
+ ssh_uri: ssh://deploy@staging.example.com/www/staging.example.com/current
51
+ ```
52
+
53
+ ## Roadmap
54
+
55
+ The following features are pending:
56
+
57
+ Providers:
58
+
59
+ - URL passed in as environment
60
+ - capistrano task
61
+ - heroku, inferred from git remotes
62
+
63
+ Features:
64
+
65
+ - Reading configuration from a remote config/database.yml
66
+ - Definable per-tool specific options, e.g. to allow pg_dump to provide a table exclusion list
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
data/bin/dbcp ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'dbcp'
7
+
8
+ Dbcp::Cli.new.start ARGV
data/dbcp.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dbcp/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "dbcp"
8
+ spec.version = Dbcp::VERSION
9
+ spec.authors = ["Gabe Martin-Dempesy"]
10
+ spec.email = ["gabetax@gmail.com"]
11
+ spec.description = %q{Copy SQL databases between application environments}
12
+ spec.summary = %q{}
13
+ spec.homepage = "https://github.com/gabetax/dbcp"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "virtus", "~> 1.0.2"
22
+ spec.add_dependency "net-ssh", "~> 2.8.0"
23
+ spec.add_dependency "net-sftp", "~> 2.1.2"
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "rspec"
26
+ spec.add_development_dependency "codeclimate-test-reporter"
27
+ spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "pry"
29
+ end
data/lib/dbcp/cli.rb ADDED
@@ -0,0 +1,53 @@
1
+ module Dbcp
2
+ class Cli
3
+ DEFAULT_DESTINATION = 'development'
4
+
5
+ def initialize(stdout = $stdout)
6
+ @logger = Logger.new stdout
7
+ @logger.formatter = Proc.new do |severity, datetime, progname, msg|
8
+ "#{datetime}: #{msg}\n"
9
+ end
10
+ end
11
+
12
+ def start(argv)
13
+ if argv.length < 1
14
+ usage
15
+ exit 1
16
+ end
17
+
18
+ begin
19
+ source = Environment.find(argv.shift)
20
+ destination = Environment.find(argv.shift || DEFAULT_DESTINATION)
21
+
22
+ if source == destination
23
+ @logger.fatal "source and destination environments are the same"
24
+ exit 3
25
+ end
26
+
27
+ if source.database.adapter != destination.database.adapter
28
+ @logger.fatal "source (#{source.database.adapter}) and destination (#{destination.database.adapter}) environments must be the same database type"
29
+ exit 4
30
+ end
31
+ rescue EnvironmentNotFound => e
32
+ @logger.fatal e.to_s
33
+ exit 2
34
+ end
35
+
36
+ @logger.info "exporting #{source.environment_name}..."
37
+ source_snapshot_file = source.export
38
+
39
+ @logger.info "transferring data..."
40
+ destination_snapshot_file = source_snapshot_file.transfer_to(destination)
41
+
42
+ @logger.info "importing #{destination_snapshot_file.path} to #{destination.environment_name}..."
43
+ destination.import destination_snapshot_file
44
+
45
+ source_snapshot_file.delete
46
+ destination_snapshot_file.delete if source_snapshot_file != destination_snapshot_file
47
+ end
48
+
49
+ def usage
50
+ @logger.fatal "Usage: #{$0} source_environment [destination_environment || #{DEFAULT_DESTINATION}]"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,32 @@
1
+ module Dbcp
2
+ class Database
3
+ class UnsupportedDatabaseAdapter < StandardError; end
4
+
5
+ class << self
6
+ def build(args)
7
+ klass_for_adapter(args['adapter']).new args
8
+ end
9
+
10
+ def klass_for_adapter(adapter)
11
+ klass = case adapter
12
+ when /mysql/
13
+ MysqlDatabase
14
+ when /postgres/
15
+ PostgresDatabase
16
+ else
17
+ raise UnsupportedDatabaseAdapter.new("Unsupported database adapter: #{adapter}")
18
+ end
19
+ end
20
+ end
21
+
22
+ include Virtus.value_object
23
+ values do
24
+ attribute :adapter
25
+ attribute :database
26
+ attribute :host, String, default: 'localhost'
27
+ attribute :socket
28
+ attribute :username
29
+ attribute :password
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,40 @@
1
+ module Dbcp
2
+ class DatabaseSnapshotFile
3
+ attr_reader :path
4
+ attr_reader :environment
5
+
6
+ def initialize(environment)
7
+ @environment = environment
8
+ @path = "/tmp/dbcp_#{Time.now.to_f}"
9
+ end
10
+
11
+ # @return [DatabaseSnapshotFile]
12
+ def transfer_to(destination_environment)
13
+ source_host = environment.execution_host
14
+ destination_host = destination_environment.execution_host
15
+
16
+ return self if source_host.local? && destination_host.local?
17
+ return self if source_host == destination_host
18
+
19
+ destination_snapshot_file = DatabaseSnapshotFile.new(destination_environment)
20
+
21
+ if source_host.local? && destination_host.remote?
22
+ destination_host.upload path, destination_snapshot_file.path
23
+
24
+ elsif source_host.remote? && destination_host.local?
25
+ source_host.download path, destination_snapshot_file.path
26
+
27
+ else
28
+ # both remote
29
+ source_host.download path, path
30
+ destination_host.upload path, destination_snapshot_file.path
31
+ end
32
+
33
+ destination_snapshot_file
34
+ end
35
+
36
+ def delete
37
+ environment.execution_host.execute %W(rm #{path}).shelljoin
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,21 @@
1
+ module Dbcp
2
+ class MysqlDatabase < Database
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
5
+ end
6
+
7
+ def import_command(snapshot_file)
8
+ %W[mysql #{socket_or_host} --user=#{username} --password=#{password} #{database}].shelljoin + ' < ' + snapshot_file.path.shellescape
9
+ end
10
+
11
+ private
12
+
13
+ def socket_or_host
14
+ if socket
15
+ "--socket=#{socket}"
16
+ else
17
+ "--host=#{host}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ module Dbcp
2
+ class PostgresDatabase < Database
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
5
+ end
6
+
7
+ 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
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,39 @@
1
+ module Dbcp
2
+ class EnvironmentNotFound < StandardError; end
3
+ class ExecutionError < StandardError; end
4
+
5
+ class Environment
6
+ ENVIRONMENT_PROVIDERS = [
7
+ DatabaseYamlEnvironmentProvider.new('config/database.yml')
8
+ ]
9
+
10
+ class << self
11
+ def find(environment_name)
12
+ ENVIRONMENT_PROVIDERS.each do |provider|
13
+ environment = provider.find environment_name
14
+ return environment if environment
15
+ end
16
+
17
+ raise EnvironmentNotFound.new "Could not locate '#{environment_name}' environment"
18
+ end
19
+ end
20
+
21
+ # coersion causes issues when assigning rspec doubles
22
+ include Virtus.value_object(coerce: false)
23
+ values do
24
+ attribute :environment_name, String
25
+ attribute :database, Database
26
+ attribute :execution_host, ExecutionHost
27
+ end
28
+
29
+ def export
30
+ DatabaseSnapshotFile.new(self).tap do |snapshot_file|
31
+ execution_host.execute database.export_command(snapshot_file)
32
+ end
33
+ end
34
+
35
+ def import(snapshot_file)
36
+ execution_host.execute database.import_command(snapshot_file)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ require 'yaml'
2
+
3
+ module Dbcp
4
+ class DatabaseYamlEnvironmentProvider
5
+ def initialize(database_yaml_path)
6
+ @database_yaml_path = database_yaml_path
7
+ end
8
+
9
+ # @return [Environment, nil]
10
+ def find(environment_name)
11
+ begin
12
+ environment_hash = read_file[environment_name]
13
+ if environment_hash
14
+ build_environment environment_name, environment_hash
15
+ else
16
+ nil
17
+ end
18
+ rescue Errno::ENOENT
19
+ return nil
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def read_file
26
+ YAML.load_file @database_yaml_path
27
+ end
28
+
29
+ def build_environment(environment_name, environment_hash)
30
+ Environment.new({
31
+ environment_name: environment_name,
32
+ database: Database.build(environment_hash),
33
+ execution_host: ExecutionHost.build(environment_hash)
34
+ })
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ module Dbcp
2
+ class ExecutionHost
3
+ class << self
4
+ def build(args)
5
+ if args['ssh_uri']
6
+ SshExecutionHost.new_from_uri args['ssh_uri']
7
+ else
8
+ LocalExecutionHost.new
9
+ end
10
+ end
11
+ end
12
+
13
+ # coersion causes issues when assigning rspec doubles
14
+ include Virtus.value_object(coerce: false)
15
+
16
+ def local?
17
+ !remote?
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module Dbcp
2
+ class LocalExecutionHost < ExecutionHost
3
+
4
+ # Cheap way for == to evaluate to `true` between `LocalExecutionHost` objects
5
+ values do
6
+ end
7
+
8
+ def remote?
9
+ false
10
+ end
11
+
12
+ def execute(command)
13
+ Kernel.system command
14
+ raise ExecutionError.new "Execution failed with exit code #{$?.exitstatus}. Command was: #{command}" unless $?.success?
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,80 @@
1
+ require 'net/ssh'
2
+ require 'net/sftp'
3
+ require 'uri'
4
+
5
+ module Dbcp
6
+ class SshExecutionHost < ExecutionHost
7
+
8
+ class << self
9
+ def new_from_uri(uri_string)
10
+ uri = URI.parse uri_string
11
+ raise URI::InvalidURIError.new "SSH URI must be in form 'ssh://username@example.com/path/to_application_root'. We received: '#{uri_string}'." unless uri.user && uri.host
12
+ new({
13
+ host: uri.host,
14
+ port: uri.port,
15
+ username: uri.user,
16
+ path: uri.path
17
+ })
18
+ end
19
+ end
20
+
21
+ values do
22
+ attribute :host
23
+ attribute :port, Fixnum
24
+ attribute :username
25
+ attribute :path
26
+ end
27
+
28
+ def remote?
29
+ true
30
+ end
31
+
32
+ def execute(command)
33
+ # http://stackoverflow.com/questions/3386233/how-to-get-exit-status-with-rubys-netssh-library
34
+ stdout_data = ""
35
+ stderr_data = ""
36
+ exitstatus = nil
37
+ exit_signal = nil
38
+
39
+ Net::SSH.start host, username do |ssh|
40
+ ssh.open_channel do |channel|
41
+ channel.exec command do |ch, success|
42
+ unless success
43
+ raise ExecutionError.new "Exection over SSH to failed for: (ssh.channel.exec)"
44
+ end
45
+ channel.on_data do |ch,data|
46
+ stdout_data+=data
47
+ end
48
+
49
+ channel.on_extended_data do |ch,type,data|
50
+ stderr_data+=data
51
+ end
52
+
53
+ channel.on_request("exit-status") do |ch,data|
54
+ exitstatus = data.read_long
55
+ end
56
+
57
+ channel.on_request("exit-signal") do |ch, data|
58
+ exit_signal = data.read_long
59
+ end
60
+ end
61
+ end
62
+ ssh.loop
63
+ end
64
+
65
+ raise ExecutionError.new "Execution failed with exit code #{$?.exitstatus}. Command was: #{command}" if exitstatus > 0
66
+ end
67
+
68
+ def download(source_path, destination_path)
69
+ Net::SFTP.start host, username do |ssh|
70
+ return ssh.download! source_path, destination_path
71
+ end
72
+ end
73
+
74
+ def upload(source_path, destination_path)
75
+ Net::SFTP.start host, username do |ssh|
76
+ return ssh.upload! source_path, destination_path
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,3 @@
1
+ module Dbcp
2
+ VERSION = "0.0.1"
3
+ end
data/lib/dbcp.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'pry'
2
+
3
+ require 'logger'
4
+ require 'virtus'
5
+ require 'dbcp/cli'
6
+ require 'dbcp/database'
7
+ require 'dbcp/databases/mysql_database'
8
+ require 'dbcp/databases/postgres_database'
9
+ require 'dbcp/environment_providers/database_yaml_environment_provider'
10
+ require 'dbcp/execution_host'
11
+ require 'dbcp/execution_hosts/local_execution_host'
12
+ require 'dbcp/execution_hosts/ssh_execution_host'
13
+ require 'dbcp/environment'
14
+ require 'dbcp/database_snapshot_file'
15
+ require 'dbcp/version'
16
+
17
+ module Dbcp
18
+ end
@@ -0,0 +1,16 @@
1
+ development:
2
+ adapter: postgresql
3
+ encoding: unicode
4
+ database: dev_database
5
+ pool: 5
6
+ username: dev_username
7
+ password: dev_password
8
+
9
+ staging:
10
+ adapter: postgresql
11
+ encoding: unicode
12
+ database: staging_database
13
+ pool: 5
14
+ username: staging_username
15
+ password: staging_password
16
+ ssh_uri: ssh://deploy@staging.example.com/www/staging.example.com/current
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dbcp::Cli do
4
+ subject { Dbcp::Cli.new silent_stdout }
5
+ let(:silent_stdout) { '/dev/null' }
6
+
7
+ before { Dir.chdir lib = File.expand_path('../../../fixtures', __FILE__) }
8
+
9
+ describe "#start" do
10
+ context "success" do
11
+ let(:source) { double 'Dbcp::Environment', database: double(adapter: 'postgres'), environment_name: 'staging' }
12
+ let(:destination) { double 'Dbcp::Environment', database: double(adapter: 'postgres'), environment_name: 'development' }
13
+ let(:source_snapshot_file) { double 'Dbcp::DatabaseSnapshotFile.new', path: '/tmp/foo', transfer_to: destination_snapshot_file }
14
+ let(:destination_snapshot_file) { double 'Dbcp::DatabaseSnapshotFile.new', path: '/tmp/bar' }
15
+
16
+ before do
17
+ allow(Dbcp::Environment).to receive(:find).with('staging') { source }
18
+ allow(Dbcp::Environment).to receive(:find).with('development') { destination }
19
+ end
20
+
21
+ it "clones the database" do
22
+ expect(source).to receive(:export) { source_snapshot_file }
23
+ expect(destination).to receive(:import).with(destination_snapshot_file)
24
+ expect(source_snapshot_file).to receive(:delete)
25
+ expect(destination_snapshot_file).to receive(:delete)
26
+
27
+ subject.start ['staging']
28
+ end
29
+ end
30
+
31
+ context "too few arguments" do
32
+ it "exits" do
33
+ expect { subject.start [] }.to raise_error(SystemExit)
34
+ end
35
+ end
36
+
37
+ context "two different database types" do
38
+ it "exist" do
39
+ expect { subject.start ['development', 'sqlite'] }.to raise_error(SystemExit)
40
+ end
41
+ end
42
+
43
+ context "environments are the same" do
44
+ it "exits" do
45
+ expect { subject.start ['development', 'development'] }.to raise_error(SystemExit)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dbcp::DatabaseSnapshotFile do
4
+ subject { Dbcp::DatabaseSnapshotFile.new environment }
5
+ let(:environment) { double 'Dbcp::Environment', execution_host: execution_host }
6
+ let(:execution_host) { double 'Dbcp::ExecutionError' }
7
+
8
+ describe "#path" do
9
+ it "defines a temporary path" do
10
+ expect(subject.path).to match '/tmp/'
11
+ end
12
+ end
13
+
14
+ describe "#transfer_to" do
15
+ let(:environment) { double 'Dbcp::Environment', execution_host: source_host }
16
+ let(:destination) { double 'Dbcp::Environment', execution_host: dest_host }
17
+
18
+ context "both local" do
19
+ let(:source_host) { Dbcp::LocalExecutionHost.new }
20
+ let(:dest_host) { Dbcp::LocalExecutionHost.new }
21
+ it "does not transfer" do
22
+ expect(subject.transfer_to destination).to eq subject
23
+ end
24
+ end
25
+
26
+ context "source local, destination remote" do
27
+ let(:source_host) { Dbcp::LocalExecutionHost.new }
28
+ let(:dest_host) { Dbcp::SshExecutionHost.new }
29
+ it "uploads to dest host" do
30
+ expect(dest_host).to receive(:upload)
31
+ subject.transfer_to destination
32
+ end
33
+ end
34
+
35
+ context "source remote, destination local" do
36
+ let(:source_host) { Dbcp::SshExecutionHost.new }
37
+ let(:dest_host) { Dbcp::LocalExecutionHost.new }
38
+ it "downloads from source host" do
39
+ expect(source_host).to receive(:download)
40
+ subject.transfer_to destination
41
+ end
42
+ end
43
+
44
+ context "both remote, SAME" do
45
+ let(:source_host) { Dbcp::SshExecutionHost.new_from_uri 'ssh://staging_user@staging.example.com:2222/www/staging/current' }
46
+ let(:dest_host) { Dbcp::SshExecutionHost.new_from_uri 'ssh://staging_user@staging.example.com:2222/www/staging/current' }
47
+ it "does not transfer" do
48
+ expect(subject.transfer_to destination).to eq subject
49
+ end
50
+ end
51
+
52
+ context "both remote, DIFFERENT" do
53
+ let(:source_host) { Dbcp::SshExecutionHost.new host: 'prd.example.com' }
54
+ let(:dest_host) { Dbcp::SshExecutionHost.new host: 'stg.example.com' }
55
+ it "downloads from source host" do
56
+ expect(source_host).to receive(:download)
57
+ expect(dest_host).to receive(:upload)
58
+ subject.transfer_to destination
59
+ end
60
+ end
61
+ end
62
+
63
+ describe "#delete" do
64
+ it "has the host execute rm" do
65
+ expect(execution_host).to receive(:execute) do |command|
66
+ expect(command).to match 'rm'
67
+ end
68
+
69
+ subject.delete
70
+ end
71
+ end
72
+
73
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dbcp::Database do
4
+ describe ".build" do
5
+ context "valid type" do
6
+ specify { expect(Dbcp::Database.build 'adapter' => 'postgresql').to be_a(Dbcp::PostgresDatabase) }
7
+ end
8
+
9
+ context "invalid type" do
10
+ specify { expect { Dbcp::Database.build 'adapter' => 'invalid'}.to raise_error(Dbcp::Database::UnsupportedDatabaseAdapter) }
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dbcp::MysqlDatabase do
4
+ subject do
5
+ Dbcp::MysqlDatabase.new({
6
+ host: 'local',
7
+ database: 'my_database',
8
+ username: 'my_user',
9
+ password: 'my_password',
10
+ })
11
+ end
12
+
13
+ let(:snapshot_file) { Dbcp::DatabaseSnapshotFile.new double }
14
+
15
+ describe "#export_command" do
16
+ specify { expect(subject.export_command snapshot_file).to match 'mysqldump' }
17
+ end
18
+
19
+ describe "#import_command" do
20
+ specify { expect(subject.import_command snapshot_file).to match 'mysql' }
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dbcp::PostgresDatabase do
4
+ subject do
5
+ Dbcp::PostgresDatabase.new({
6
+ host: 'local',
7
+ database: 'my_database',
8
+ username: 'my_user',
9
+ password: 'my_password',
10
+ })
11
+ end
12
+
13
+ let(:snapshot_file) { Dbcp::DatabaseSnapshotFile.new double }
14
+
15
+ describe "#export_command" do
16
+ specify { expect(subject.export_command snapshot_file).to match 'pg_dump' }
17
+ end
18
+
19
+ describe "#import_command" do
20
+ specify { expect(subject.import_command snapshot_file).to match 'pg_restore' }
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dbcp::DatabaseYamlEnvironmentProvider do
4
+ subject { Dbcp::DatabaseYamlEnvironmentProvider.new path }
5
+ let(:path) { File.expand_path('../../../../fixtures/config/database.yml', __FILE__) }
6
+
7
+ describe "#find" do
8
+ context "when environment exists" do
9
+ it "returns an environment" do
10
+ environment = subject.find 'development'
11
+ expect(environment).to be_a Dbcp::Environment
12
+ expect(environment.database).to be_a Dbcp::Database
13
+ expect(environment.environment_name).to eq 'development'
14
+ expect(environment.database.adapter).to eq 'postgresql'
15
+ expect(environment.database.database).to eq 'dev_database'
16
+ expect(environment.database.username).to eq 'dev_username'
17
+ expect(environment.database.password).to eq 'dev_password'
18
+ end
19
+
20
+ context "without ssh_uri" do
21
+ it "executes on localhost" do
22
+ environment = subject.find 'development'
23
+ expect(environment.execution_host).to be_a Dbcp::LocalExecutionHost
24
+ end
25
+ end
26
+
27
+ context "with ssh_uri" do
28
+ it "executes on remote host" do
29
+ environment = subject.find 'staging'
30
+ expect(environment.execution_host).to be_a Dbcp::SshExecutionHost
31
+ end
32
+ end
33
+ end
34
+ context "when environment doesn't exist" do
35
+ specify { expect(subject.find 'does-not-exist').to be_nil }
36
+ end
37
+ context "when file doesn't exist" do
38
+ let(:path) { File.expand_path('../../../../fixtures/config/database-does-not-exist.yml', __FILE__) }
39
+ specify { expect(subject.find 'development').to be_nil }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dbcp::Environment do
4
+
5
+ describe ".find" do
6
+ context "when not found" do
7
+ specify { expect { Dbcp::Environment.find 'does-not-exist' }.to raise_error(Dbcp::EnvironmentNotFound) }
8
+ end
9
+
10
+ context "when found" do
11
+ let(:environment) { double }
12
+ before { allow_any_instance_of(Dbcp::DatabaseYamlEnvironmentProvider).to receive(:find).and_return(environment) }
13
+ specify { expect(Dbcp::Environment.find 'development').to eq environment }
14
+ end
15
+ end
16
+
17
+
18
+ describe "import/export" do
19
+ subject { Dbcp::Environment.new database: database, execution_host: execution_host }
20
+ let(:database) { double 'Dbcp::Database', export_command: double, import_command: double }
21
+ let(:execution_host) { double 'Dbcp::ExecutionHost', execute: nil }
22
+ # before { allow(Kernel).to receive(:system) }
23
+
24
+ describe "#export" do
25
+ it "executes the database's export command" do
26
+ subject.export
27
+ expect(subject.execution_host).to have_received(:execute).with(database.export_command)
28
+ end
29
+
30
+ it "returns the snapshot file" do
31
+ expect(subject.export).to be_a Dbcp::DatabaseSnapshotFile
32
+ end
33
+ end
34
+
35
+ describe "#import" do
36
+ let(:snapshot_file) { double }
37
+ it "executes the database's import command" do
38
+ subject.import snapshot_file
39
+ expect(subject.execution_host).to have_received(:execute).with(database.import_command)
40
+ end
41
+ end
42
+ end
43
+
44
+
45
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dbcp::ExecutionHost do
4
+ describe ".build" do
5
+ context "when ssh information is present" do
6
+ let(:environment_hash) { { 'ssh_uri' => 'ssh://staging_user@staging.example.com/www/staging/current' } }
7
+ specify { expect(Dbcp::ExecutionHost.build(environment_hash)).to be_a Dbcp::SshExecutionHost }
8
+ end
9
+
10
+ context "without ssh information" do
11
+ specify { expect(Dbcp::ExecutionHost.build({})).to be_a Dbcp::LocalExecutionHost }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dbcp::LocalExecutionHost do
4
+
5
+ describe "==" do
6
+ specify { expect(Dbcp::LocalExecutionHost.new).to eq Dbcp::LocalExecutionHost.new }
7
+ end
8
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dbcp::SshExecutionHost do
4
+ subject { Dbcp::SshExecutionHost.new_from_uri 'ssh://staging_user@staging.example.com:2222/www/staging/current' }
5
+ describe ".new_from_uri" do
6
+ context "with valid uri" do
7
+ specify do
8
+ host = Dbcp::SshExecutionHost.new_from_uri 'ssh://staging_user@staging.example.com:2222/www/staging/current'
9
+ expect(host).to be_a Dbcp::SshExecutionHost
10
+ expect(host.host).to eq 'staging.example.com'
11
+ expect(host.port).to eq 2222
12
+ expect(host.username).to eq 'staging_user'
13
+ expect(host.path).to eq '/www/staging/current'
14
+ end
15
+ end
16
+
17
+ context "with invalid uri" do
18
+ specify do
19
+ expect { Dbcp::SshExecutionHost.new_from_uri 'staging.example.com' }.to raise_error URI::InvalidURIError
20
+ end
21
+ end
22
+ end
23
+
24
+ describe "#execute" do
25
+ # Not sure how to either easily unit test, or securely/portably integration test. Suggested appreciated.
26
+ end
27
+
28
+ describe "#download" do
29
+ let(:path) { '/tmp/foo' }
30
+ it "uses ssh" do
31
+ expect(Net::SFTP).to receive(:start).with('staging.example.com', 'staging_user') { true }
32
+ subject.download path, path
33
+ end
34
+ end
35
+
36
+ describe "#upload" do
37
+ let(:path) { '/tmp/foo' }
38
+ it "uses SFTP" do
39
+ expect(Net::SFTP).to receive(:start).with('staging.example.com', 'staging_user') { true }
40
+ subject.upload path, path
41
+ end
42
+ end
43
+
44
+
45
+ end
@@ -0,0 +1,9 @@
1
+ if ENV['CODECLIMATE_REPO_TOKEN']
2
+ require "codeclimate-test-reporter"
3
+ CodeClimate::TestReporter.start
4
+ end
5
+
6
+ require_relative '../lib/dbcp'
7
+
8
+ Dir[File.expand_path("../support/**/*.rb", __FILE__)].sort.each { |f| require f }
9
+
File without changes
metadata ADDED
@@ -0,0 +1,203 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dbcp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Gabe Martin-Dempesy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: virtus
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-ssh
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 2.8.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 2.8.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: net-sftp
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 2.1.2
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 2.1.2
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: codeclimate-test-reporter
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Copy SQL databases between application environments
126
+ email:
127
+ - gabetax@gmail.com
128
+ executables:
129
+ - dbcp
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - .travis.yml
135
+ - Gemfile
136
+ - LICENSE.txt
137
+ - README.md
138
+ - Rakefile
139
+ - bin/dbcp
140
+ - dbcp.gemspec
141
+ - lib/dbcp.rb
142
+ - lib/dbcp/cli.rb
143
+ - lib/dbcp/database.rb
144
+ - lib/dbcp/database_snapshot_file.rb
145
+ - lib/dbcp/databases/mysql_database.rb
146
+ - lib/dbcp/databases/postgres_database.rb
147
+ - lib/dbcp/environment.rb
148
+ - lib/dbcp/environment_providers/database_yaml_environment_provider.rb
149
+ - lib/dbcp/execution_host.rb
150
+ - lib/dbcp/execution_hosts/local_execution_host.rb
151
+ - lib/dbcp/execution_hosts/ssh_execution_host.rb
152
+ - lib/dbcp/version.rb
153
+ - spec/fixtures/config/database.yml
154
+ - spec/lib/dbcp/cli_spec.rb
155
+ - spec/lib/dbcp/database_snapshot_file_spec.rb
156
+ - spec/lib/dbcp/database_spec.rb
157
+ - spec/lib/dbcp/databases/mysql_database_spec.rb
158
+ - spec/lib/dbcp/databases/postgres_database_spec.rb
159
+ - spec/lib/dbcp/environment_providers/database_yaml_environment_provider_spec.rb
160
+ - spec/lib/dbcp/environment_spec.rb
161
+ - spec/lib/dbcp/execution_host_spec.rb
162
+ - spec/lib/dbcp/execution_hosts/local_execution_host_spec.rb
163
+ - spec/lib/dbcp/execution_hosts/ssh_execution_host_spec.rb
164
+ - spec/spec_helper.rb
165
+ - spec/support/.keep
166
+ homepage: https://github.com/gabetax/dbcp
167
+ licenses:
168
+ - MIT
169
+ metadata: {}
170
+ post_install_message:
171
+ rdoc_options: []
172
+ require_paths:
173
+ - lib
174
+ required_ruby_version: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - '>='
177
+ - !ruby/object:Gem::Version
178
+ version: '0'
179
+ required_rubygems_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - '>='
182
+ - !ruby/object:Gem::Version
183
+ version: '0'
184
+ requirements: []
185
+ rubyforge_project:
186
+ rubygems_version: 2.0.6
187
+ signing_key:
188
+ specification_version: 4
189
+ summary: ''
190
+ test_files:
191
+ - spec/fixtures/config/database.yml
192
+ - spec/lib/dbcp/cli_spec.rb
193
+ - spec/lib/dbcp/database_snapshot_file_spec.rb
194
+ - spec/lib/dbcp/database_spec.rb
195
+ - spec/lib/dbcp/databases/mysql_database_spec.rb
196
+ - spec/lib/dbcp/databases/postgres_database_spec.rb
197
+ - spec/lib/dbcp/environment_providers/database_yaml_environment_provider_spec.rb
198
+ - spec/lib/dbcp/environment_spec.rb
199
+ - spec/lib/dbcp/execution_host_spec.rb
200
+ - spec/lib/dbcp/execution_hosts/local_execution_host_spec.rb
201
+ - spec/lib/dbcp/execution_hosts/ssh_execution_host_spec.rb
202
+ - spec/spec_helper.rb
203
+ - spec/support/.keep