duple 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES.md ADDED
@@ -0,0 +1,13 @@
1
+ ## 0.0.2 (October 21, 2012) - Fix local db config
2
+
3
+ * Load db connection details from the config file.
4
+ * Added some minimal class-level documentation.
5
+
6
+ ## 0.0.1 (October 19, 2012) - Initial release
7
+
8
+ * Implemented refresh from local to heroku and from heroku to local
9
+ * Implemented heroku to heroku refresh
10
+ * Implemented structure command
11
+ * Implemented config tasks
12
+ * Implemented copy command
13
+ * Added dry-run flag
data/README.md CHANGED
@@ -26,31 +26,31 @@ figure the application. Read and modify it, or clear it out and write your own.
26
26
 
27
27
  # Resets the stage database
28
28
  # Loads the latest production snapshot into the stage database
29
- $ duple refresh production stage
29
+ $ duple refresh -s production -t stage
30
30
 
31
31
  # Downloads the latest full snapshot from production
32
32
  # Resets the development database
33
33
  # Loads the snapshot into the development database
34
- $ duple refresh production development
34
+ $ duple refresh -s production -t development
35
35
 
36
36
  # Captures a new production database snapshot
37
37
  # Downloads the latest full snapshot from production
38
38
  # Resets the development database
39
39
  # Loads the snapshot into the development database
40
- $ duple refresh production development --capture
40
+ $ duple refresh -s production -t development --capture
41
41
 
42
42
  # Downloads the schema and a subset of data from stage
43
43
  # Resets the backstage database
44
44
  # Loads the structure and the subset into the backstage database
45
- $ duple refresh stage backstage --group minimal
45
+ $ duple refresh -s stage -t backstage --group minimal
46
46
 
47
47
  # Downloads the data from the specified tables from the stage database
48
48
  # Loads the data into the backstage database
49
- $ duple copy stage backstage --tables products categories
49
+ $ duple copy -s stage -t backstage --tables products categories
50
50
 
51
51
  ## Future
52
52
 
53
- * Everything.
53
+ * Pre- and post- refresh hooks
54
54
  * Support for skipping pre- and post- refresh steps.
55
55
  * Support for running pre- and post- refresh steps by themselves.
56
56
  * Support for other data stores.
@@ -1,5 +1,25 @@
1
1
  module Duple
2
2
  module CLI
3
+
4
+ # Usage:
5
+ # duple config [COMMAND]
6
+ #
7
+ # Options:
8
+ # -c, [--config=CONFIG] # The location of the config file.
9
+ #
10
+ # Manage your configuration.
11
+ #
12
+ # Tasks:
13
+ # duple config all # Prints the current configuration.
14
+ # duple config environments # Prints the environment configurations.
15
+ # duple config groups # Prints the group configurations.
16
+ # duple config help [COMMAND] # Describe subcommands or one specific subcommand
17
+ # duple config other # Prints other options.
18
+ # duple config post-refresh # Prints the post-refresh tasks.
19
+ # duple config pre-refresh # Prints the pre-refresh tasks.
20
+
21
+ # Options:
22
+ # -c, [--config=CONFIG] # The location of the config file.
3
23
  class Config < Thor
4
24
  include Duple::CLI::Helpers
5
25
 
@@ -1,5 +1,19 @@
1
1
  module Duple
2
2
  module CLI
3
+
4
+ # Usage:
5
+ # duple copy
6
+ #
7
+ # Options:
8
+ # -c, [--config=CONFIG] # The location of the config file.
9
+ # -s, [--source=SOURCE] # The name of the source environment.
10
+ # -t, [--target=TARGET] # The name of the target environment.
11
+ # -g, [--group=GROUP] # The group configuration to use when dumping source data.
12
+ # [--capture] # Capture a new source snapshot before refreshing.
13
+ # --dry-run, [--dry-run] # Perform a dry run of the command. No data will be moved.
14
+ # -t, [--tables=one two three] # A list of tables to include when dumping source data.
15
+ #
16
+ # Copies data from a source to a target database.
3
17
  class Copy < Thor::Group
4
18
  include Duple::CLI::Helpers
5
19
 
@@ -18,11 +32,11 @@ module Duple
18
32
  end
19
33
 
20
34
  def dump_data
21
- postgres.pg_dump(dump_flags, data_file_path, source_credentials)
35
+ postgres.pg_dump(dump_flags, data_file_path, source_db_config)
22
36
  end
23
37
 
24
38
  def restore_data
25
- postgres.pg_restore('-e -v --no-acl -O -a', data_file_path, target_credentials)
39
+ postgres.pg_restore('-e -v --no-acl -O -a', data_file_path, target_db_config)
26
40
  end
27
41
  end
28
42
  end
@@ -38,7 +38,7 @@ module Duple
38
38
 
39
39
  def group_option
40
40
  class_option :group,
41
- desc: 'Name of the group configuration to use when dumping source data.',
41
+ desc: 'The group configuration to use when dumping source data.',
42
42
  type: :string,
43
43
  aliases: '-g'
44
44
  end
@@ -119,16 +119,40 @@ module Duple
119
119
  @structure_file_path ||= File.join(dump_dir_path, filename)
120
120
  end
121
121
 
122
- def fetch_heroku_credentials(appname)
122
+ def source_db_config
123
+ @source_db_config ||= if config.heroku_source?
124
+ fetch_heroku_db_config(source_appname)
125
+ else
126
+ config.db_config(config.source_name)
127
+ end
128
+ end
129
+
130
+ def target_db_config
131
+ @target_db_config ||= if config.heroku_target?
132
+ fetch_heroku_db_config(target_appname)
133
+ else
134
+ config.db_config(config.target_name)
135
+ end
136
+ end
137
+
138
+ def fetch_heroku_db_config(appname)
139
+ # Run the heroku config command first, even if it's a dry run. So
140
+ # that the command to get the config will show up in the dry run log.
123
141
  config_vars = heroku.capture(appname, "config")
124
142
 
125
- return config.dry_run_credentials(appname) if config.dry_run?
143
+ if config.dry_run?
144
+ config.db_config(appname, dry_run: true)
145
+ else
146
+ parse_heroku_config(config_vars)
147
+ end
148
+ end
126
149
 
150
+ def parse_heroku_config(config_vars)
127
151
  db_url = config_vars.split("\n").detect { |l| l =~ /DATABASE_URL/ }
128
152
  raise ArgumentError.new("Missing DATABASE_URL variable for #{appname}") if db_url.nil?
129
153
 
130
154
  db_url.match(
131
- /postgres:\/\/(?<user>.*):(?<password>.*)@(?<host>.*):(?<port>\d*)\/(?<db>.*)/
155
+ /postgres:\/\/(?<username>.*):(?<password>.*)@(?<host>.*):(?<port>\d*)\/(?<database>.*)/
132
156
  )
133
157
  end
134
158
 
@@ -159,22 +183,6 @@ module Duple
159
183
  flags = [ '-Fc -a', include_flags, exclude_flags ].flatten.compact.join(' ')
160
184
  end
161
185
 
162
- def source_credentials
163
- @source_credentials ||= if config.heroku_source?
164
- fetch_heroku_credentials(source_appname)
165
- else
166
- config.local_credentials
167
- end
168
- end
169
-
170
- def target_credentials
171
- @target_credentials ||= if config.heroku_target?
172
- fetch_heroku_credentials(target_appname)
173
- else
174
- config.local_credentials
175
- end
176
- end
177
-
178
186
  def config
179
187
  @config ||= parse_config
180
188
  end
@@ -1,5 +1,13 @@
1
1
  module Duple
2
2
  module CLI
3
+
4
+ # Usage:
5
+ # duple init
6
+ #
7
+ # Options:
8
+ # -c, [--config=CONFIG] # The location of the config file.
9
+ #
10
+ # Generates a sample configuration file.
3
11
  class Init < Thor::Group
4
12
  include Duple::CLI::Helpers
5
13
 
@@ -1,5 +1,18 @@
1
1
  module Duple
2
2
  module CLI
3
+
4
+ # Usage:
5
+ # duple refresh
6
+ #
7
+ # Options:
8
+ # -c, [--config=CONFIG] # The location of the config file.
9
+ # -s, [--source=SOURCE] # The name of the source environment.
10
+ # -t, [--target=TARGET] # The name of the target environment.
11
+ # -g, [--group=GROUP] # The group configuration to use when dumping source data.
12
+ # [--capture] # Capture a new source snapshot before refreshing.
13
+ # -t, [--tables=one two three] # A list of tables to include when dumping source data.
14
+ #
15
+ # Resets and copies schema and data from a source to a target database
3
16
  class Refresh < Thor::Group
4
17
  include Duple::CLI::Helpers
5
18
 
@@ -54,7 +67,7 @@ module Duple
54
67
  def dump_data
55
68
  return unless dump_data?
56
69
 
57
- postgres.pg_dump(dump_flags, data_file_path, source_credentials)
70
+ postgres.pg_dump(dump_flags, data_file_path, source_db_config)
58
71
  end
59
72
 
60
73
  def reset_target
@@ -63,9 +76,9 @@ module Duple
63
76
 
64
77
  def restore_database
65
78
  if download_snapshot?
66
- postgres.pg_restore('-e -v --no-acl -O -a', @snapshot_path, target_credentials)
79
+ postgres.pg_restore('-e -v --no-acl -O -a', @snapshot_path, target_db_config)
67
80
  elsif dump_data?
68
- postgres.pg_restore('-e -v --no-acl -O -a', data_file_path, target_credentials)
81
+ postgres.pg_restore('-e -v --no-acl -O -a', data_file_path, target_db_config)
69
82
  else
70
83
  heroku.run(target_appname, "pgbackups:restore DATABASE #{@source_snapshot_url}")
71
84
  end
@@ -12,6 +12,8 @@ require 'duple/heroku_runner'
12
12
 
13
13
  module Duple
14
14
  module CLI
15
+
16
+ # Configures the high-level structure of the CLI sub-commands.
15
17
  class Root < Thor
16
18
  include Duple::CLI::Helpers
17
19
 
@@ -1,5 +1,15 @@
1
1
  module Duple
2
2
  module CLI
3
+
4
+ # Usage:
5
+ # duple structure
6
+ #
7
+ # Options:
8
+ # -c, [--config=CONFIG] # The location of the config file.
9
+ # -s, [--source=SOURCE] # The name of the source environment.
10
+ # -t, [--target=TARGET] # The name of the target environment.
11
+ #
12
+ # Copies structure from a source to a target database.
3
13
  class Structure < Thor::Group
4
14
  include Duple::CLI::Helpers
5
15
 
@@ -8,7 +18,7 @@ module Duple
8
18
  target_option
9
19
 
10
20
  def dump_structure
11
- postgres.pg_dump('-Fc --no-acl -O -s', structure_file_path, source_credentials)
21
+ postgres.pg_dump('-Fc --no-acl -O -s', structure_file_path, source_db_config)
12
22
  end
13
23
 
14
24
  def reset_target
@@ -16,7 +26,7 @@ module Duple
16
26
  end
17
27
 
18
28
  def load_structure
19
- postgres.pg_restore('-v --no-acl -O -s', structure_file_path, target_credentials)
29
+ postgres.pg_restore('-v --no-acl -O -s', structure_file_path, target_db_config)
20
30
  end
21
31
  end
22
32
  end
@@ -125,24 +125,38 @@ module Duple
125
125
  tables
126
126
  end
127
127
 
128
- def dry_run_credentials(envname)
128
+ def db_config(appname, options = nil)
129
+ options ||= {}
130
+ options = {dry_run: false}.merge(options)
131
+
132
+ if options[:dry_run]
133
+ db_config_for_dry_run(appname)
134
+ else
135
+ db_config_for_app(appname)
136
+ end
137
+ end
138
+
139
+ def db_config_for_dry_run(appname)
129
140
  {
130
- user: "[#{envname}.USER]",
141
+ username: "[#{envname}.USER]",
131
142
  password: "[#{envname}.PASS]",
132
143
  host: "[#{envname}.HOST]",
133
144
  port: "[#{envname}.PORT]",
134
- db: "[#{envname}.DB]"
145
+ database: "[#{envname}.DB]"
135
146
  }
136
147
  end
137
148
 
138
- def local_credentials
139
- # TODO: Override these defaults from the config
149
+ def db_config_for_app(appname)
150
+ env = environments[appname]
151
+ if env['database'].nil?
152
+ raise ArgumentError.new('Invalid config: "database" is required for a local environment.')
153
+ end
140
154
  {
141
- user: 'postgres',
142
- password: '',
143
- host: 'localhost',
144
- port: '5432',
145
- db: 'duple_development'
155
+ username: env['username'] || 'postgres',
156
+ password: env['password'] || '',
157
+ host: env['host'] || 'localhost',
158
+ port: env['port'] || '5432',
159
+ database: env['database']
146
160
  }
147
161
  end
148
162
 
@@ -1,4 +1,7 @@
1
1
  module Duple
2
+
3
+ # Decorates a Duple::Runner instance with helper methods for executing
4
+ # Heroku commands.
2
5
  class HerokuRunner
3
6
  def initialize(runner)
4
7
  @runner = runner
@@ -1,25 +1,28 @@
1
1
  module Duple
2
+
3
+ # Decorates a Duple::Runner instance with helper methods for executing
4
+ # PostgreSQL commands.
2
5
  class PGRunner
3
6
  def initialize(runner)
4
7
  @runner = runner
5
8
  end
6
9
 
7
- def pg_dump(flags, dump_file, credentials)
8
- pg_command('pg_dump', flags, credentials, nil, "> #{dump_file}")
10
+ def pg_dump(flags, dump_file, db_config)
11
+ pg_command('pg_dump', flags, db_config, nil, "> #{dump_file}")
9
12
  end
10
13
 
11
- def pg_restore(flags, dump_file, credentials)
12
- pg_command('pg_restore', flags, credentials, '-d', "< #{dump_file}")
14
+ def pg_restore(flags, dump_file, db_config)
15
+ pg_command('pg_restore', flags, db_config, '-d', "< #{dump_file}")
13
16
  end
14
17
 
15
- def pg_command(pg_command, flags, credentials, db_flag, tail = nil)
18
+ def pg_command(pg_command, flags, db_config, db_flag, tail = nil)
16
19
  command = []
17
- command << %{PGPASSWORD="#{credentials[:password]}"}
20
+ command << %{PGPASSWORD="#{db_config[:password]}"}
18
21
  command << pg_command
19
22
  command << flags
20
- command << %{-h #{credentials[:host]} -U #{credentials[:user]} -p #{credentials[:port]}}
23
+ command << %{-h #{db_config[:host]} -U #{db_config[:username]} -p #{db_config[:port]}}
21
24
  command << db_flag if db_flag
22
- command << credentials[:db]
25
+ command << db_config[:database]
23
26
  command << tail if tail
24
27
  @runner.run(command.join(' '))
25
28
  end
data/lib/duple/runner.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'fileutils'
2
2
 
3
3
  module Duple
4
+
5
+ # Helper class for executing shell commands.
4
6
  class Runner
5
7
 
6
8
  def initialize(options = nil)
data/lib/duple/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Duple
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -16,7 +16,7 @@ describe Duple::CLI::Copy do
16
16
  }
17
17
 
18
18
  context 'with neither tables nor group option' do
19
- it 'fetches the source credentials' do
19
+ it 'fetches the source db config' do
20
20
  expect {
21
21
  invoke_copy(tables: nil)
22
22
  }.to raise_error(ArgumentError, 'One of --group or --tables options is required.')
@@ -27,7 +27,7 @@ describe Duple::CLI::Copy do
27
27
  let(:source) { 'stage' }
28
28
  let(:target) { 'development' }
29
29
 
30
- it 'fetches the source credentials' do
30
+ it 'fetches the source db config' do
31
31
  runner.should_receive(:capture).with("heroku config -a duple-stage")
32
32
  .and_return(heroku_config_response)
33
33
 
@@ -53,14 +53,14 @@ describe Duple::CLI::Copy do
53
53
  let(:source) { 'production' }
54
54
  let(:target) { 'stage' }
55
55
 
56
- it 'fetches the source credentials' do
56
+ it 'fetches the source db config' do
57
57
  runner.should_receive(:capture).with("heroku config -a duple-production")
58
58
  .and_return(heroku_config_response)
59
59
 
60
60
  invoke_copy
61
61
  end
62
62
 
63
- it 'fetches the target credentials' do
63
+ it 'fetches the target db config' do
64
64
  runner.should_receive(:capture).with("heroku config -a duple-stage")
65
65
  .and_return(heroku_config_response)
66
66
 
@@ -86,7 +86,7 @@ describe Duple::CLI::Copy do
86
86
  let(:source) { 'development' }
87
87
  let(:target) { 'stage' }
88
88
 
89
- it 'fetches the target credentials' do
89
+ it 'fetches the target db config' do
90
90
  runner.should_receive(:capture).with("heroku config -a duple-stage")
91
91
  .and_return(heroku_config_response)
92
92
 
@@ -18,7 +18,7 @@ describe Duple::CLI::Structure do
18
18
  let(:source) { 'stage' }
19
19
  let(:target) { 'development' }
20
20
 
21
- it 'fetches the source credentials' do
21
+ it 'fetches the source db config' do
22
22
  runner.should_receive(:capture).with("heroku config -a duple-stage")
23
23
  .and_return(heroku_config_response)
24
24
 
@@ -57,14 +57,14 @@ describe Duple::CLI::Structure do
57
57
  let(:source) { 'production' }
58
58
  let(:target) { 'stage' }
59
59
 
60
- it 'fetches the source credentials' do
60
+ it 'fetches the source db config' do
61
61
  runner.should_receive(:capture).with("heroku config -a duple-production")
62
62
  .and_return(heroku_config_response)
63
63
 
64
64
  invoke_structure
65
65
  end
66
66
 
67
- it 'fetches the target credentials' do
67
+ it 'fetches the target db config' do
68
68
  runner.should_receive(:capture).with("heroku config -a duple-stage")
69
69
  .and_return(heroku_config_response)
70
70
 
@@ -103,7 +103,7 @@ describe Duple::CLI::Structure do
103
103
  let(:source) { 'development' }
104
104
  let(:target) { 'stage' }
105
105
 
106
- it 'fetches the target credentials' do
106
+ it 'fetches the target db config' do
107
107
  runner.should_receive(:capture).with("heroku config -a duple-stage")
108
108
  .and_return(heroku_config_response)
109
109
 
@@ -2,6 +2,54 @@ require 'spec_helper'
2
2
 
3
3
  describe Duple::Configuration do
4
4
 
5
+
6
+ describe '#db_config' do
7
+ let(:config_hash) { YAML.load(File.read('spec/config/simple.yml'))}
8
+
9
+ it 'supplies default database values for a local environment' do
10
+ config = Duple::Configuration.new(config_hash, {})
11
+ c = config.db_config('development')
12
+ c[:username].should == 'postgres'
13
+ c[:password].should == ''
14
+ c[:host].should == 'localhost'
15
+ c[:port].should == '5432'
16
+ end
17
+
18
+ context 'with all db config parameters' do
19
+ let(:db_config_hash) {
20
+ db_hash = config_hash['environments']['development']
21
+ db_hash['username'] = 'config_username'
22
+ db_hash['password'] = 'config_password'
23
+ db_hash['host'] = 'config_host'
24
+ db_hash['port'] = '6022'
25
+ db_hash['database'] = 'config_database'
26
+ config_hash
27
+ }
28
+ let(:config) { Duple::Configuration.new(db_config_hash, {}) }
29
+ subject(:db_config) { config.db_config('development') }
30
+
31
+ it 'loads the options from the config' do
32
+ db_config[:username].should == 'config_username'
33
+ db_config[:password].should == 'config_password'
34
+ db_config[:host].should == 'config_host'
35
+ db_config[:port].should == '6022'
36
+ db_config[:database].should == 'config_database'
37
+ end
38
+ end
39
+
40
+ it 'fails if the database name is missing' do
41
+ config_hash['environments']['development'].delete('database')
42
+ config = Duple::Configuration.new(config_hash, {})
43
+
44
+ expect {
45
+ config.db_config('development')
46
+ }.to raise_error(
47
+ ArgumentError,
48
+ 'Invalid config: "database" is required for a local environment.'
49
+ )
50
+ end
51
+ end
52
+
5
53
  describe '#excluded_tables' do
6
54
  let(:config_hash) { YAML.load(File.read('spec/config/groups.yml'))}
7
55
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duple
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-19 00:00:00.000000000 Z
12
+ date: 2012-10-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
@@ -119,6 +119,7 @@ files:
119
119
  - .gitignore
120
120
  - .rspec
121
121
  - .simplecov
122
+ - CHANGES.md
122
123
  - Gemfile
123
124
  - LICENSE.txt
124
125
  - README.md