duple 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +5 -1
- data/Rakefile +2 -1
- data/lib/duple.rb +3 -0
- data/lib/duple/cli/config.rb +6 -3
- data/lib/duple/cli/helpers.rb +36 -57
- data/lib/duple/cli/refresh.rb +20 -9
- data/lib/duple/config/command.rb +46 -0
- data/lib/duple/config/task.rb +14 -0
- data/lib/duple/configuration.rb +11 -14
- data/lib/duple/endpoint.rb +92 -0
- data/lib/duple/runner.rb +0 -4
- data/lib/duple/source.rb +14 -0
- data/lib/duple/target.rb +14 -0
- data/lib/duple/version.rb +1 -1
- data/spec/config/groups.yml +0 -2
- data/spec/config/kitchensink.yml +6 -12
- data/spec/config/simple.yml +0 -2
- data/spec/config/tasks.yml +0 -2
- data/spec/duple/cli/config_spec.rb +7 -1
- data/spec/duple/cli/refresh_spec.rb +9 -7
- data/spec/duple/configuration_spec.rb +0 -1
- data/spec/duple/runner_spec.rb +9 -0
- data/spec/support/cli_spec_helpers.rb +12 -0
- data/templates/config/duple.yml +43 -50
- metadata +7 -2
data/README.md
CHANGED
@@ -50,7 +50,11 @@ figure the application. Read and modify it, or clear it out and write your own.
|
|
50
50
|
|
51
51
|
## Future
|
52
52
|
|
53
|
-
*
|
53
|
+
* Custom error classes.
|
54
|
+
* Improve documentation
|
55
|
+
* Support non-Heroku remote PostgreSQL servers.
|
56
|
+
* Move knowledge of endpoint types out of Thor tasks.
|
57
|
+
|
54
58
|
* Support for skipping pre- and post- refresh steps.
|
55
59
|
* Support for running pre- and post- refresh steps by themselves.
|
56
60
|
* Support for other data stores.
|
data/Rakefile
CHANGED
@@ -10,9 +10,10 @@ begin
|
|
10
10
|
|
11
11
|
desc "Run cane to check quality metrics"
|
12
12
|
Cane::RakeTask.new(:cane) do |cane|
|
13
|
+
cane.abc_max = 15
|
13
14
|
cane.style_measure = 100
|
14
15
|
cane.style_glob = '{lib}/**/*.rb'
|
15
|
-
cane.gte = {'coverage/covered_percent' =>
|
16
|
+
cane.gte = {'coverage/covered_percent' => 98}
|
16
17
|
end
|
17
18
|
rescue LoadError
|
18
19
|
warn "cane not available, quality task not provided."
|
data/lib/duple.rb
CHANGED
data/lib/duple/cli/config.rb
CHANGED
@@ -33,12 +33,15 @@ module Duple
|
|
33
33
|
say
|
34
34
|
end
|
35
35
|
|
36
|
-
def print_tasks(header,
|
36
|
+
def print_tasks(header, tasks)
|
37
37
|
say header
|
38
38
|
say '-' * 80
|
39
|
-
|
39
|
+
tasks.each do |task|
|
40
40
|
say
|
41
|
-
say ' ' +
|
41
|
+
say ' ' + task.name
|
42
|
+
commands = task.commands.map do |c|
|
43
|
+
{ subject: c.subject, command_type: c.type, command: c.command }
|
44
|
+
end
|
42
45
|
print_table commands, indent: 4
|
43
46
|
end
|
44
47
|
say
|
data/lib/duple/cli/helpers.rb
CHANGED
@@ -69,8 +69,16 @@ module Duple
|
|
69
69
|
end
|
70
70
|
|
71
71
|
module InstanceMethods
|
72
|
-
def
|
73
|
-
|
72
|
+
def config
|
73
|
+
@config ||= parse_config
|
74
|
+
end
|
75
|
+
|
76
|
+
def parse_config
|
77
|
+
config_path = File.join(destination_root, app_config_path)
|
78
|
+
config_data = File.read(config_path)
|
79
|
+
erbed = ERB.new(config_data).result
|
80
|
+
config_hash = YAML.load(erbed) || {}
|
81
|
+
Duple::Configuration.new(config_hash, options)
|
74
82
|
end
|
75
83
|
|
76
84
|
def app_config_path(verify_file = true)
|
@@ -81,89 +89,71 @@ module Duple
|
|
81
89
|
config_path
|
82
90
|
end
|
83
91
|
|
92
|
+
def default_config_path
|
93
|
+
File.join('config', 'duple.yml')
|
94
|
+
end
|
95
|
+
|
96
|
+
def source
|
97
|
+
@source ||= Duple::Source.new(config, runner)
|
98
|
+
end
|
99
|
+
|
100
|
+
def target
|
101
|
+
@target ||= Duple::Target.new(config, runner)
|
102
|
+
end
|
103
|
+
|
84
104
|
def runner
|
85
105
|
@runner ||= Duple::Runner.new(dry_run: config.dry_run?)
|
86
106
|
end
|
87
107
|
|
88
108
|
def postgres
|
109
|
+
# TODO: This should go away as the methods are moved into the endpoint classes.
|
89
110
|
@pg_runner ||= Duple::PGRunner.new(runner)
|
90
111
|
end
|
91
112
|
|
92
113
|
def heroku
|
114
|
+
# TODO: This should go away as the methods are moved into the endpoint classes.
|
93
115
|
@heroku ||= Duple::HerokuRunner.new(runner)
|
94
116
|
end
|
95
117
|
|
96
|
-
def source_appname
|
97
|
-
@source_appname ||= config.heroku_name(config.source_environment)
|
98
|
-
end
|
99
|
-
|
100
118
|
def target_appname
|
101
|
-
|
119
|
+
# TODO: This should go away as the methods are moved into the endpoint classes.
|
120
|
+
target.appname
|
102
121
|
end
|
103
122
|
|
104
123
|
def dump_dir_path
|
124
|
+
# TODO: This should go on the configuration and be configurable.
|
105
125
|
File.join('tmp', 'duple')
|
106
126
|
end
|
107
127
|
|
108
128
|
def data_file_path
|
129
|
+
# TODO: This should go on the configuration and be configurable.
|
109
130
|
@data_file_path ||= File.join(dump_dir_path, "#{config.source_name}-data.dump")
|
110
131
|
end
|
111
132
|
|
112
133
|
def structure_file_path
|
134
|
+
# TODO: This should go on the configuration and be configurable.
|
113
135
|
filename = "#{config.source_name}-structure.dump"
|
114
136
|
@structure_file_path ||= File.join(dump_dir_path, filename)
|
115
137
|
end
|
116
138
|
|
117
139
|
def snapshot_file_path(timestamp)
|
140
|
+
# TODO: This should go on the configuration and be configurable.
|
118
141
|
filename = "#{config.source_name}-#{timestamp}.dump"
|
119
142
|
@structure_file_path ||= File.join(dump_dir_path, filename)
|
120
143
|
end
|
121
144
|
|
122
145
|
def source_db_config
|
123
|
-
|
124
|
-
|
125
|
-
else
|
126
|
-
config.db_config(config.source_name)
|
127
|
-
end
|
146
|
+
# TODO: This should go away as the methods are moved into the endpoint classes.
|
147
|
+
@source_db_config ||= source.db_config
|
128
148
|
end
|
129
149
|
|
130
150
|
def target_db_config
|
131
|
-
|
132
|
-
|
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.
|
141
|
-
config_vars = heroku.capture(appname, "config")
|
142
|
-
|
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
|
149
|
-
|
150
|
-
def parse_heroku_config(config_vars)
|
151
|
-
db_url = config_vars.split("\n").detect { |l| l =~ /DATABASE_URL/ }
|
152
|
-
raise ArgumentError.new("Missing DATABASE_URL variable for #{appname}") if db_url.nil?
|
153
|
-
|
154
|
-
db_url.match(
|
155
|
-
/postgres:\/\/(?<username>.*):(?<password>.*)@(?<host>.*):(?<port>\d*)\/(?<database>.*)/
|
156
|
-
)
|
157
|
-
end
|
158
|
-
|
159
|
-
def fetch_latest_snapshot_time(appname)
|
160
|
-
response = heroku.capture(appname, 'pgbackups')
|
161
|
-
last_line = response.split("\n").last
|
162
|
-
timestring = last_line.match(/\w+\s+(?<timestamp>[\d\s\/\:\.]+)\s+.*/)[:timestamp]
|
163
|
-
DateTime.strptime(timestring, '%Y/%m/%d %H:%M.%S')
|
151
|
+
# TODO: This should go away as the methods are moved into the endpoint classes.
|
152
|
+
@target_db_config ||= target.db_config
|
164
153
|
end
|
165
154
|
|
166
155
|
def reset_database(env)
|
156
|
+
# TODO: This should be moved to the endpoint classes.
|
167
157
|
if config.heroku?(env)
|
168
158
|
appname = config.heroku_name(env)
|
169
159
|
heroku.run(appname, 'pg:reset')
|
@@ -174,6 +164,7 @@ module Duple
|
|
174
164
|
end
|
175
165
|
|
176
166
|
def dump_flags
|
167
|
+
# TODO: This should be moved to the endpoint classes.
|
177
168
|
include_tables = config.included_tables
|
178
169
|
include_flags = include_tables.map { |t| "-t #{t}" }
|
179
170
|
|
@@ -182,18 +173,6 @@ module Duple
|
|
182
173
|
|
183
174
|
flags = [ '-Fc -a', include_flags, exclude_flags ].flatten.compact.join(' ')
|
184
175
|
end
|
185
|
-
|
186
|
-
def config
|
187
|
-
@config ||= parse_config
|
188
|
-
end
|
189
|
-
|
190
|
-
def parse_config
|
191
|
-
config_path = File.join(destination_root, app_config_path)
|
192
|
-
config_data = File.read(config_path)
|
193
|
-
erbed = ERB.new(config_data).result
|
194
|
-
config_hash = YAML.load(erbed) || {}
|
195
|
-
Duple::Configuration.new(config_hash, options)
|
196
|
-
end
|
197
176
|
end
|
198
177
|
end
|
199
178
|
end
|
data/lib/duple/cli/refresh.rb
CHANGED
@@ -24,10 +24,6 @@ module Duple
|
|
24
24
|
tables_option
|
25
25
|
|
26
26
|
no_tasks do
|
27
|
-
def capture_snapshot?
|
28
|
-
config.capture? && config.heroku_source?
|
29
|
-
end
|
30
|
-
|
31
27
|
def fetch_snapshot_url?
|
32
28
|
config.heroku_source? && !config.filtered_tables?
|
33
29
|
end
|
@@ -39,24 +35,35 @@ module Duple
|
|
39
35
|
def dump_data?
|
40
36
|
config.local_source? || config.filtered_tables?
|
41
37
|
end
|
38
|
+
|
39
|
+
def run_tasks(tasks)
|
40
|
+
tasks.each do |task|
|
41
|
+
task.commands.each do |c|
|
42
|
+
source.execute(c) if c.source?
|
43
|
+
target.execute(c) if c.target?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
42
47
|
end
|
43
48
|
|
44
|
-
def
|
45
|
-
|
49
|
+
def run_prerefresh_tasks
|
50
|
+
run_tasks(config.pre_refresh_tasks)
|
51
|
+
end
|
46
52
|
|
47
|
-
|
53
|
+
def capture_snapshot
|
54
|
+
source.capture_snapshot if config.capture?
|
48
55
|
end
|
49
56
|
|
50
57
|
def fetch_snapshot_url
|
51
58
|
return unless fetch_snapshot_url?
|
52
59
|
|
53
|
-
@source_snapshot_url =
|
60
|
+
@source_snapshot_url = source.snapshot_url
|
54
61
|
end
|
55
62
|
|
56
63
|
def download_snapshot
|
57
64
|
return unless download_snapshot?
|
58
65
|
|
59
|
-
timestamp =
|
66
|
+
timestamp = source.latest_snapshot_time
|
60
67
|
|
61
68
|
@snapshot_path = snapshot_file_path(timestamp.strftime('%Y-%m-%d-%H-%M-%S'))
|
62
69
|
unless File.exists?(@snapshot_path)
|
@@ -83,6 +90,10 @@ module Duple
|
|
83
90
|
heroku.run(target_appname, "pgbackups:restore DATABASE #{@source_snapshot_url}")
|
84
91
|
end
|
85
92
|
end
|
93
|
+
|
94
|
+
def run_postrefresh_tasks
|
95
|
+
run_tasks(config.post_refresh_tasks)
|
96
|
+
end
|
86
97
|
end
|
87
98
|
end
|
88
99
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Duple
|
2
|
+
module Config
|
3
|
+
# Represents a command that can be executed in the source or target environment.
|
4
|
+
class Command
|
5
|
+
SHELL = 'shell'
|
6
|
+
HEROKU = 'heroku'
|
7
|
+
VALID_TYPES = [SHELL, HEROKU]
|
8
|
+
|
9
|
+
SOURCE = 'source'
|
10
|
+
TARGET = 'target'
|
11
|
+
VALID_SUBJECTS = [SOURCE, TARGET]
|
12
|
+
|
13
|
+
attr_accessor :subject, :type, :command
|
14
|
+
|
15
|
+
def initialize(config_hash)
|
16
|
+
@command = config_hash['command']
|
17
|
+
@type = config_hash['command_type']
|
18
|
+
@subject = config_hash['subject']
|
19
|
+
|
20
|
+
unless VALID_TYPES.include?(@type)
|
21
|
+
raise ArgumentError.new("Invalid config: #{@type} is not a valid command type.")
|
22
|
+
end
|
23
|
+
|
24
|
+
unless VALID_SUBJECTS.include?(@subject)
|
25
|
+
raise ArgumentError.new("Invalid config: #{@subject} is not a valid command subject.")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def source?
|
30
|
+
subject == Duple::Config::Command::SOURCE
|
31
|
+
end
|
32
|
+
|
33
|
+
def target?
|
34
|
+
subject == Duple::Config::Command::TARGET
|
35
|
+
end
|
36
|
+
|
37
|
+
def heroku?
|
38
|
+
type == Duple::Config::Command::HEROKU
|
39
|
+
end
|
40
|
+
|
41
|
+
def shell?
|
42
|
+
type == Duple::Config::Command::SHELL
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'duple/config/command'
|
2
|
+
|
3
|
+
module Duple
|
4
|
+
module Config
|
5
|
+
# Represents a set of commands that can be executed in the source or target environment.
|
6
|
+
class Task
|
7
|
+
attr_accessor :name, :commands
|
8
|
+
def initialize(name, command_list)
|
9
|
+
@name = name
|
10
|
+
@commands = command_list.map { |c| Duple::Config::Command.new(c) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/duple/configuration.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'duple/config/task'
|
2
|
+
|
1
3
|
module Duple
|
2
4
|
|
3
5
|
# Represents the configuration that will be used to perform the data
|
@@ -181,23 +183,11 @@ module Duple
|
|
181
183
|
end
|
182
184
|
|
183
185
|
def pre_refresh_tasks
|
184
|
-
raw_config['pre_refresh']
|
185
|
-
end
|
186
|
-
|
187
|
-
def pre_refresh_task(task_name)
|
188
|
-
task = pre_refresh_tasks[task_name]
|
189
|
-
raise ArgumentError.new("Invalid pre_refresh task: #{task_name}") if task.nil?
|
190
|
-
task
|
186
|
+
@pre_refresh_tasks ||= build_tasks(raw_config['pre_refresh'])
|
191
187
|
end
|
192
188
|
|
193
189
|
def post_refresh_tasks
|
194
|
-
raw_config['post_refresh']
|
195
|
-
end
|
196
|
-
|
197
|
-
def post_refresh_task(task_name)
|
198
|
-
task = post_refresh_tasks[task_name]
|
199
|
-
raise ArgumentError.new("Invalid post_refresh task: #{task_name}") if task.nil?
|
200
|
-
task
|
190
|
+
@post_refresh_tasks ||= build_tasks(raw_config['post_refresh'])
|
201
191
|
end
|
202
192
|
|
203
193
|
def other_options
|
@@ -206,6 +196,13 @@ module Duple
|
|
206
196
|
|
207
197
|
protected
|
208
198
|
|
199
|
+
def build_tasks(task_hash)
|
200
|
+
return [] if task_hash.nil?
|
201
|
+
task_hash.map do |name, command_list|
|
202
|
+
Duple::Config::Task.new(name, command_list)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
209
206
|
def env_names_by_flag(flag_name, flag_value, allow_multiple = false)
|
210
207
|
matching_envs = environments.select { |n, c| c[flag_name] == flag_value }
|
211
208
|
if matching_envs.size > 1 && !allow_multiple
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Duple
|
2
|
+
module Endpoint
|
3
|
+
def self.included(base)
|
4
|
+
base.send(:attr_reader, :config, :runner)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(config, runner)
|
8
|
+
@config = config
|
9
|
+
@runner = runner
|
10
|
+
if config.heroku?(environment)
|
11
|
+
extend Duple::HerokuEndpoint
|
12
|
+
else
|
13
|
+
extend Duple::PostgreSQLEndpoint
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module HerokuEndpoint
|
19
|
+
def appname
|
20
|
+
@appname ||= config.heroku_name(environment)
|
21
|
+
end
|
22
|
+
|
23
|
+
def heroku
|
24
|
+
@heroku ||= Duple::HerokuRunner.new(runner)
|
25
|
+
end
|
26
|
+
|
27
|
+
def db_config
|
28
|
+
@db_config ||= fetch_db_config
|
29
|
+
end
|
30
|
+
|
31
|
+
def snapshot_url
|
32
|
+
@snapshot_url ||= heroku.capture(appname, 'pgbackups:url').strip
|
33
|
+
end
|
34
|
+
|
35
|
+
def latest_snapshot_time
|
36
|
+
unless @snapshot_time
|
37
|
+
response = heroku.capture(appname, 'pgbackups')
|
38
|
+
last_line = response.split("\n").last
|
39
|
+
timestring = last_line.match(/\w+\s+(?<timestamp>[\d\s\/\:\.]+)\s+.*/)[:timestamp]
|
40
|
+
@snapshot_time = DateTime.strptime(timestring, '%Y/%m/%d %H:%M.%S')
|
41
|
+
end
|
42
|
+
@snapshot_time
|
43
|
+
end
|
44
|
+
|
45
|
+
def fetch_db_config
|
46
|
+
# Run the heroku config command first, even if it's a dry run, so
|
47
|
+
# that the command to get the config will show up in the dry run log.
|
48
|
+
config_vars = heroku.capture(appname, "config")
|
49
|
+
|
50
|
+
if config.dry_run?
|
51
|
+
config.db_config(name, dry_run: true)
|
52
|
+
else
|
53
|
+
parse_config(config_vars)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_config(config_vars)
|
58
|
+
db_url = config_vars.split("\n").detect { |l| l =~ /DATABASE_URL/ }
|
59
|
+
raise ArgumentError.new("Missing DATABASE_URL variable for #{appname}") if db_url.nil?
|
60
|
+
|
61
|
+
db_url.match(
|
62
|
+
/postgres:\/\/(?<username>.*):(?<password>.*)@(?<host>.*):(?<port>\d*)\/(?<database>.*)/
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
def capture_snapshot
|
67
|
+
heroku.run(appname, 'pgbackups:capture')
|
68
|
+
end
|
69
|
+
|
70
|
+
def execute(cmd)
|
71
|
+
if cmd.shell?
|
72
|
+
heroku.run(appname, "run \"#{cmd.command}\"")
|
73
|
+
elsif cmd.heroku?
|
74
|
+
heroku.run(appname, cmd.command)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
module PostgreSQLEndpoint
|
80
|
+
def db_config
|
81
|
+
config.db_config(name)
|
82
|
+
end
|
83
|
+
|
84
|
+
def capture_snapshot
|
85
|
+
# Do nothing. When we have non-local PostgreSQL endpoints, this will need to be implemented.
|
86
|
+
end
|
87
|
+
|
88
|
+
def execute(cmd)
|
89
|
+
runner.run(cmd.command) if cmd.shell?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/duple/runner.rb
CHANGED
data/lib/duple/source.rb
ADDED
data/lib/duple/target.rb
ADDED
data/lib/duple/version.rb
CHANGED
data/spec/config/groups.yml
CHANGED
data/spec/config/kitchensink.yml
CHANGED
@@ -1,6 +1,3 @@
|
|
1
|
-
dump_dir: tmp
|
2
|
-
postgres_user: <%= ENV['POSTGRES_DEV_USER'] || 'postgres' %>
|
3
|
-
|
4
1
|
environments:
|
5
2
|
development:
|
6
3
|
type: local
|
@@ -42,24 +39,21 @@ post_refresh:
|
|
42
39
|
command: ps:scale worker=1
|
43
40
|
migrate:
|
44
41
|
- subject: target
|
45
|
-
command_type:
|
46
|
-
command: db:migrate
|
42
|
+
command_type: shell
|
43
|
+
command: rake db:migrate
|
47
44
|
scrub_data:
|
48
45
|
- subject: target
|
49
|
-
command_type:
|
50
|
-
command: snapshot:scrub
|
46
|
+
command_type: shell
|
47
|
+
command: rake snapshot:scrub
|
51
48
|
test_data:
|
52
49
|
- subject: target
|
53
|
-
command_type:
|
54
|
-
command: snapshot:setup_testers
|
50
|
+
command_type: shell
|
51
|
+
command: rake snapshot:setup_testers
|
55
52
|
|
56
53
|
groups:
|
57
54
|
all:
|
58
55
|
include_all: true # Include all tables
|
59
56
|
|
60
|
-
structure:
|
61
|
-
exclude_all: true # Exclude all tables (structure only)
|
62
|
-
|
63
57
|
minimal:
|
64
58
|
include_tables: # Include only the listed tables
|
65
59
|
- categories
|
data/spec/config/simple.yml
CHANGED
data/spec/config/tasks.yml
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Duple::CLI::Config do
|
4
|
+
let(:script) { Duple::CLI::Config.new }
|
5
|
+
it 'raises an error if an invalid config path is supplied' do
|
6
|
+
expect {
|
7
|
+
script.invoke(:all, [], {config: 'spec/config/nonexistent.yml'})
|
8
|
+
}.to raise_error(ArgumentError, 'Missing config file: spec/config/nonexistent.yml')
|
9
|
+
end
|
10
|
+
|
4
11
|
it 'prints the configuration' do
|
5
12
|
result = capture_stdout do
|
6
|
-
script = Duple::CLI::Config.new
|
7
13
|
script.invoke(:all, [], {config: 'spec/config/kitchensink.yml'})
|
8
14
|
end
|
9
15
|
|
@@ -293,9 +293,8 @@ describe Duple::CLI::Refresh do
|
|
293
293
|
end
|
294
294
|
|
295
295
|
context 'with pre-refresh tasks' do
|
296
|
-
before { pending 'Implement pre-refresh tasks' }
|
297
|
-
|
298
296
|
before {
|
297
|
+
stub_prerefresh_tasks
|
299
298
|
stub_fetch_url
|
300
299
|
stub_fetch_config
|
301
300
|
stub_fetch_backups
|
@@ -305,6 +304,7 @@ describe Duple::CLI::Refresh do
|
|
305
304
|
stub_reset_local
|
306
305
|
stub_restore_url
|
307
306
|
stub_restore_data
|
307
|
+
stub_postrefresh_tasks
|
308
308
|
}
|
309
309
|
|
310
310
|
let(:source) { 'production' }
|
@@ -315,7 +315,7 @@ describe Duple::CLI::Refresh do
|
|
315
315
|
runner.should_receive(:run).once.ordered.with('heroku maintenance:on -a duple-stage')
|
316
316
|
runner.should_receive(:run).any_number_of_times.ordered.with(/heroku pgbackups/)
|
317
317
|
|
318
|
-
invoke_refresh(
|
318
|
+
invoke_refresh(task_options.merge(capture: true))
|
319
319
|
end
|
320
320
|
|
321
321
|
it 'executes the tasks in order' do
|
@@ -352,8 +352,9 @@ describe Duple::CLI::Refresh do
|
|
352
352
|
end
|
353
353
|
|
354
354
|
context 'with post-refresh tasks' do
|
355
|
-
before { pending 'Implement post-refresh tasks' }
|
355
|
+
# before { pending 'Implement post-refresh tasks' }
|
356
356
|
before {
|
357
|
+
stub_prerefresh_tasks
|
357
358
|
stub_fetch_url
|
358
359
|
stub_fetch_config
|
359
360
|
stub_fetch_backups
|
@@ -363,6 +364,7 @@ describe Duple::CLI::Refresh do
|
|
363
364
|
stub_reset_local
|
364
365
|
stub_restore_url
|
365
366
|
stub_restore_data
|
367
|
+
stub_postrefresh_tasks
|
366
368
|
}
|
367
369
|
|
368
370
|
let(:source) { 'production' }
|
@@ -371,9 +373,9 @@ describe Duple::CLI::Refresh do
|
|
371
373
|
|
372
374
|
it 'executes the tasks after refreshing' do
|
373
375
|
runner.should_receive(:run).any_number_of_times.ordered.with(/heroku pgbackups/)
|
374
|
-
runner.should_receive(:run).once.ordered.with('heroku maintenance:
|
376
|
+
runner.should_receive(:run).once.ordered.with('heroku maintenance:off -a duple-stage')
|
375
377
|
|
376
|
-
invoke_refresh(
|
378
|
+
invoke_refresh(task_options.merge(capture: true))
|
377
379
|
end
|
378
380
|
|
379
381
|
it 'executes the tasks in order' do
|
@@ -401,7 +403,7 @@ describe Duple::CLI::Refresh do
|
|
401
403
|
|
402
404
|
it 'executes the tasks in order' do
|
403
405
|
runner.should_receive(:run).once.ordered.with('rake refresh:finish')
|
404
|
-
runner.should_receive(:run).once.ordered.with('heroku maintenance:
|
406
|
+
runner.should_receive(:run).once.ordered.with('heroku maintenance:off -a duple-stage')
|
405
407
|
runner.should_receive(:run).once.ordered.with('heroku run "rake refresh:finish" -a duple-stage')
|
406
408
|
|
407
409
|
invoke_refresh(task_options)
|
data/spec/duple/runner_spec.rb
CHANGED
@@ -61,5 +61,14 @@ describe Duple::Runner do
|
|
61
61
|
runner.capture('date')
|
62
62
|
recorder.string.split("\n").should == ['ls tmp', 'date']
|
63
63
|
end
|
64
|
+
|
65
|
+
context 'that is not I/O-like' do
|
66
|
+
let(:recorder) { "" }
|
67
|
+
it 'raises an error' do
|
68
|
+
expect {
|
69
|
+
runner.capture('date')
|
70
|
+
}.to raise_error(ArgumentError, 'Invalid :recorder option: ')
|
71
|
+
end
|
72
|
+
end
|
64
73
|
end
|
65
74
|
end
|
@@ -25,6 +25,18 @@ module Duple
|
|
25
25
|
File.read('spec/config/heroku_config.txt')
|
26
26
|
end
|
27
27
|
|
28
|
+
def stub_prerefresh_tasks
|
29
|
+
runner.stub(:run).with(/heroku run "rake refresh:prepare/)
|
30
|
+
runner.stub(:run).with(/rake refresh:prepare/)
|
31
|
+
runner.stub(:run).with(/heroku maintenance:on/)
|
32
|
+
end
|
33
|
+
|
34
|
+
def stub_postrefresh_tasks
|
35
|
+
runner.stub(:run).with(/heroku run "rake refresh:finish/)
|
36
|
+
runner.stub(:run).with(/rake refresh:finish/)
|
37
|
+
runner.stub(:run).with(/heroku maintenance:off/)
|
38
|
+
end
|
39
|
+
|
28
40
|
def stub_fetch_url
|
29
41
|
runner.stub(:capture).with(/heroku pgbackups:url/)
|
30
42
|
.and_return(heroku_pgbackups_url_response)
|
data/templates/config/duple.yml
CHANGED
@@ -1,71 +1,64 @@
|
|
1
|
-
dump_dir: tmp
|
2
|
-
postgres_user: <%= ENV['POSTGRES_DEV_USER'] || 'postgres' %>
|
3
|
-
|
4
1
|
environments:
|
5
2
|
development:
|
6
|
-
type: local
|
7
|
-
default_target: true
|
3
|
+
type: local # The "development" database is hosted on your machine.
|
4
|
+
default_target: true # Use "development" as the default data target.
|
5
|
+
database: duple_development # The name of the "development" database is "duple_development"
|
8
6
|
|
9
7
|
backstage:
|
10
|
-
type: heroku
|
11
|
-
appname: duple-backstage
|
8
|
+
type: heroku # The "backstage" database is hosted on Heroku.
|
9
|
+
appname: duple-backstage # The "backstage" app is called "duple-backstage" on Heroku.
|
12
10
|
|
13
11
|
stage:
|
14
|
-
type: heroku
|
15
|
-
appname: duple-stage
|
16
|
-
default_source: true
|
12
|
+
type: heroku # The "stage" database is hosted on Heroku.
|
13
|
+
appname: duple-stage # The "stage" app is called "duple-stage" on Heroku.
|
14
|
+
default_source: true # Use "stage" as the default data source.
|
17
15
|
|
18
16
|
production:
|
19
|
-
type: heroku
|
20
|
-
appname: duple-production
|
21
|
-
allow_target: false
|
17
|
+
type: heroku # The "production" database is hosted on Heroku.
|
18
|
+
appname: duple-production # The "production" app is called "duple-production" on Heroku.
|
19
|
+
allow_target: false # The "production" database cannot be used as a data target.
|
22
20
|
|
23
21
|
pre_refresh:
|
24
|
-
|
25
|
-
- subject:
|
26
|
-
command_type:
|
27
|
-
command:
|
28
|
-
|
29
|
-
|
30
|
-
|
22
|
+
prepare_source: # Defines a set of commands called "prepare_source"
|
23
|
+
- subject: source # Execute "rake refresh:prepare" in the source shell.
|
24
|
+
command_type: shell
|
25
|
+
command: rake refresh:prepare
|
26
|
+
prepare_target: # Defines a set of commands called "prepare_target"
|
27
|
+
- subject: target # Execute "heroku maintenance:on" on the target.
|
28
|
+
command_type: heroku # Note: The command will not be executed in a local
|
29
|
+
command: maintenance:on # environment.
|
30
|
+
- subject: target # Execute "rake refresh:prepare" on the target.
|
31
|
+
command_type: shell
|
32
|
+
command: rake refresh:prepare
|
31
33
|
|
32
34
|
post_refresh:
|
33
|
-
|
34
|
-
- subject:
|
35
|
-
command_type:
|
36
|
-
command:
|
37
|
-
|
35
|
+
finish_source: # Defines a set of commands called "finish_source"
|
36
|
+
- subject: source # Execute "rake refresh:finish" in the source shell.
|
37
|
+
command_type: shell
|
38
|
+
command: rake refresh:finish
|
39
|
+
|
40
|
+
finish_target: # Defines a set of commands called "finish_source"
|
41
|
+
- subject: target # Execute "heroku maintenance:off" on the target.
|
38
42
|
command_type: heroku
|
39
43
|
command: maintenance:off
|
40
|
-
- subject: target
|
41
|
-
command_type:
|
42
|
-
command:
|
43
|
-
migrate:
|
44
|
-
- subject: target
|
45
|
-
command_type: rake
|
46
|
-
command: db:migrate
|
47
|
-
scrub_data:
|
48
|
-
- subject: target
|
49
|
-
command_type: rake
|
50
|
-
command: snapshot:scrub
|
51
|
-
test_data:
|
52
|
-
- subject: target
|
53
|
-
command_type: rake
|
54
|
-
command: snapshot:setup_testers
|
44
|
+
- subject: target # Execute "rake refresh:finish" on the target.
|
45
|
+
command_type: shell
|
46
|
+
command: rake refresh:finish
|
55
47
|
|
56
48
|
groups:
|
57
|
-
all:
|
58
|
-
include_all: true #
|
49
|
+
all: # Defines a group called "all"
|
50
|
+
include_all: true # Include all tables
|
59
51
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
minimal:
|
64
|
-
include_tables: # Include only the listed tables
|
52
|
+
minimal: # Defines a group called "minimal"
|
53
|
+
include_tables: # Include only the listed tables
|
65
54
|
- categories
|
66
55
|
- links
|
67
56
|
|
68
|
-
no_comments:
|
69
|
-
exclude_tables: #
|
57
|
+
no_comments: # Defines a group called "no_comments"
|
58
|
+
exclude_tables: # Include all but the listed tables
|
70
59
|
- comments
|
71
|
-
|
60
|
+
|
61
|
+
all_but_comments: # Defines a group called "all_but_comments"
|
62
|
+
include_all: true # Include all tables...
|
63
|
+
exclude_tables: # Except for comments
|
64
|
+
- comments # NOTE: This behaves identically to the no_comments group.
|
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.
|
4
|
+
version: 0.0.4
|
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-
|
12
|
+
date: 2012-10-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thor
|
@@ -134,10 +134,15 @@ files:
|
|
134
134
|
- lib/duple/cli/refresh.rb
|
135
135
|
- lib/duple/cli/root.rb
|
136
136
|
- lib/duple/cli/structure.rb
|
137
|
+
- lib/duple/config/command.rb
|
138
|
+
- lib/duple/config/task.rb
|
137
139
|
- lib/duple/configuration.rb
|
140
|
+
- lib/duple/endpoint.rb
|
138
141
|
- lib/duple/heroku_runner.rb
|
139
142
|
- lib/duple/pg_runner.rb
|
140
143
|
- lib/duple/runner.rb
|
144
|
+
- lib/duple/source.rb
|
145
|
+
- lib/duple/target.rb
|
141
146
|
- lib/duple/version.rb
|
142
147
|
- spec/config/groups.yml
|
143
148
|
- spec/config/heroku_config.txt
|