turbot 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/README.md +36 -0
- data/bin/turbot +17 -0
- data/data/cacert.pem +3988 -0
- data/lib/turbot/auth.rb +315 -0
- data/lib/turbot/cli.rb +38 -0
- data/lib/turbot/client/cisaurus.rb +25 -0
- data/lib/turbot/client/pgbackups.rb +113 -0
- data/lib/turbot/client/rendezvous.rb +111 -0
- data/lib/turbot/client/ssl_endpoint.rb +25 -0
- data/lib/turbot/client/turbot_postgresql.rb +148 -0
- data/lib/turbot/client.rb +757 -0
- data/lib/turbot/command/auth.rb +85 -0
- data/lib/turbot/command/base.rb +192 -0
- data/lib/turbot/command/bots.rb +326 -0
- data/lib/turbot/command/config.rb +123 -0
- data/lib/turbot/command/help.rb +179 -0
- data/lib/turbot/command/keys.rb +115 -0
- data/lib/turbot/command/logs.rb +34 -0
- data/lib/turbot/command/ssl.rb +43 -0
- data/lib/turbot/command/status.rb +51 -0
- data/lib/turbot/command/update.rb +47 -0
- data/lib/turbot/command/version.rb +23 -0
- data/lib/turbot/command.rb +304 -0
- data/lib/turbot/deprecated/help.rb +38 -0
- data/lib/turbot/deprecated.rb +5 -0
- data/lib/turbot/distribution.rb +9 -0
- data/lib/turbot/errors.rb +28 -0
- data/lib/turbot/excon.rb +11 -0
- data/lib/turbot/helpers/log_displayer.rb +70 -0
- data/lib/turbot/helpers/pg_dump_restore.rb +115 -0
- data/lib/turbot/helpers/turbot_postgresql.rb +213 -0
- data/lib/turbot/helpers.rb +521 -0
- data/lib/turbot/plugin.rb +165 -0
- data/lib/turbot/updater.rb +171 -0
- data/lib/turbot/version.rb +3 -0
- data/lib/turbot.rb +19 -0
- data/lib/vendor/turbot/okjson.rb +598 -0
- data/spec/helper/legacy_help.rb +16 -0
- data/spec/helper/pg_dump_restore_spec.rb +67 -0
- data/spec/schemas/dummy_schema.json +12 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +220 -0
- data/spec/support/display_message_matcher.rb +49 -0
- data/spec/support/dummy_api.rb +120 -0
- data/spec/support/openssl_mock_helper.rb +8 -0
- data/spec/support/organizations_mock_helper.rb +11 -0
- data/spec/turbot/auth_spec.rb +214 -0
- data/spec/turbot/client/pgbackups_spec.rb +43 -0
- data/spec/turbot/client/rendezvous_spec.rb +62 -0
- data/spec/turbot/client/ssl_endpoint_spec.rb +48 -0
- data/spec/turbot/client/turbot_postgresql_spec.rb +71 -0
- data/spec/turbot/client_spec.rb +548 -0
- data/spec/turbot/command/auth_spec.rb +38 -0
- data/spec/turbot/command/base_spec.rb +66 -0
- data/spec/turbot/command/bots_spec.rb +54 -0
- data/spec/turbot/command/config_spec.rb +143 -0
- data/spec/turbot/command/help_spec.rb +90 -0
- data/spec/turbot/command/keys_spec.rb +117 -0
- data/spec/turbot/command/logs_spec.rb +60 -0
- data/spec/turbot/command/status_spec.rb +48 -0
- data/spec/turbot/command/version_spec.rb +16 -0
- data/spec/turbot/command_spec.rb +131 -0
- data/spec/turbot/helpers/turbot_postgresql_spec.rb +181 -0
- data/spec/turbot/helpers_spec.rb +48 -0
- data/spec/turbot/plugin_spec.rb +172 -0
- data/spec/turbot/updater_spec.rb +44 -0
- data/templates/manifest.json +7 -0
- data/templates/scraper.py +5 -0
- data/templates/scraper.rb +6 -0
- metadata +199 -0
@@ -0,0 +1,304 @@
|
|
1
|
+
require 'turbot/helpers'
|
2
|
+
require 'turbot/plugin'
|
3
|
+
require 'turbot/version'
|
4
|
+
require "optparse"
|
5
|
+
require 'excon'
|
6
|
+
|
7
|
+
module Turbot
|
8
|
+
module Command
|
9
|
+
class CommandFailed < RuntimeError; end
|
10
|
+
|
11
|
+
extend Turbot::Helpers
|
12
|
+
|
13
|
+
def self.load
|
14
|
+
Dir[File.join(File.dirname(__FILE__), "command", "*.rb")].each do |file|
|
15
|
+
require file
|
16
|
+
end
|
17
|
+
Turbot::Plugin.load!
|
18
|
+
unregister_commands_made_private_after_the_fact
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.commands
|
22
|
+
@@commands ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.command_aliases
|
26
|
+
@@command_aliases ||= {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.files
|
30
|
+
@@files ||= Hash.new {|hash,key| hash[key] = File.readlines(key).map {|line| line.strip}}
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.namespaces
|
34
|
+
@@namespaces ||= {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.register_command(command)
|
38
|
+
commands[command[:command]] = command
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.unregister_commands_made_private_after_the_fact
|
42
|
+
commands.values \
|
43
|
+
.select { |c| c[:klass].private_method_defined? c[:method] } \
|
44
|
+
.each { |c| commands.delete c[:command] }
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.register_namespace(namespace)
|
48
|
+
namespaces[namespace[:name]] = namespace
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.current_command
|
52
|
+
@current_command
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.current_command=(new_current_command)
|
56
|
+
@current_command = new_current_command
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.current_args
|
60
|
+
@current_args
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.current_options
|
64
|
+
@current_options ||= {}
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.global_options
|
68
|
+
@global_options ||= []
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.invalid_arguments
|
72
|
+
@invalid_arguments
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.shift_argument
|
76
|
+
# dup argument to get a non-frozen string
|
77
|
+
@invalid_arguments.shift.dup rescue nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.validate_arguments!
|
81
|
+
unless invalid_arguments.empty?
|
82
|
+
arguments = invalid_arguments.map {|arg| "\"#{arg}\""}
|
83
|
+
if arguments.length == 1
|
84
|
+
message = "Invalid argument: #{arguments.first}"
|
85
|
+
elsif arguments.length > 1
|
86
|
+
message = "Invalid arguments: "
|
87
|
+
message << arguments[0...-1].join(", ")
|
88
|
+
message << " and "
|
89
|
+
message << arguments[-1]
|
90
|
+
end
|
91
|
+
$stderr.puts(format_with_bang(message))
|
92
|
+
run(current_command, ["--help"])
|
93
|
+
exit(1)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.warnings
|
98
|
+
@warnings ||= []
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.display_warnings
|
102
|
+
unless warnings.empty?
|
103
|
+
$stderr.puts(warnings.map {|warning| " ! #{warning}"}.join("\n"))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.global_option(name, *args, &blk)
|
108
|
+
# args.sort.reverse gives -l, --long order
|
109
|
+
global_options << { :name => name.to_s, :args => args.sort.reverse, :proc => blk }
|
110
|
+
end
|
111
|
+
|
112
|
+
global_option :bot, "-b", "--bot APP" do |bot|
|
113
|
+
raise OptionParser::InvalidOption.new(bot) if bot == "pp"
|
114
|
+
end
|
115
|
+
|
116
|
+
global_option :org, "-o", "--org ORG" do |org|
|
117
|
+
raise OptionParser::InvalidOption.new(org) if org == "rg"
|
118
|
+
end
|
119
|
+
global_option :personal, "-p", "--personal"
|
120
|
+
|
121
|
+
global_option :confirm, "--confirm APP"
|
122
|
+
global_option :help, "-h", "--help"
|
123
|
+
global_option :remote, "-r", "--remote REMOTE"
|
124
|
+
|
125
|
+
def self.prepare_run(cmd, args=[])
|
126
|
+
command = parse(cmd)
|
127
|
+
|
128
|
+
if args.include?('-h') || args.include?('--help')
|
129
|
+
args.unshift(cmd) unless cmd =~ /^-.*/
|
130
|
+
cmd = 'help'
|
131
|
+
command = parse(cmd)
|
132
|
+
end
|
133
|
+
|
134
|
+
if cmd == '--version'
|
135
|
+
cmd = 'version'
|
136
|
+
command = parse(cmd)
|
137
|
+
end
|
138
|
+
|
139
|
+
@current_command = cmd
|
140
|
+
@anonymized_args, @normalized_args = [], []
|
141
|
+
|
142
|
+
opts = {}
|
143
|
+
invalid_options = []
|
144
|
+
|
145
|
+
parser = OptionParser.new do |parser|
|
146
|
+
# remove OptionParsers Officious['version'] to avoid conflicts
|
147
|
+
# see: https://github.com/ruby/ruby/blob/trunk/lib/optparse.rb#L814
|
148
|
+
parser.base.long.delete('version')
|
149
|
+
(global_options + (command && command[:options] || [])).each do |option|
|
150
|
+
parser.on(*option[:args]) do |value|
|
151
|
+
if option[:proc]
|
152
|
+
option[:proc].call(value)
|
153
|
+
end
|
154
|
+
opts[option[:name].gsub('-', '_').to_sym] = value
|
155
|
+
ARGV.join(' ') =~ /(#{option[:args].map {|arg| arg.split(' ', 2).first}.join('|')})/
|
156
|
+
@anonymized_args << "#{$1} _"
|
157
|
+
@normalized_args << "#{option[:args].last.split(' ', 2).first} _"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
begin
|
163
|
+
parser.order!(args) do |nonopt|
|
164
|
+
invalid_options << nonopt
|
165
|
+
@anonymized_args << '!'
|
166
|
+
@normalized_args << '!'
|
167
|
+
end
|
168
|
+
rescue OptionParser::InvalidOption => ex
|
169
|
+
invalid_options << ex.args.first
|
170
|
+
@anonymized_args << '!'
|
171
|
+
@normalized_args << '!'
|
172
|
+
retry
|
173
|
+
end
|
174
|
+
|
175
|
+
args.concat(invalid_options)
|
176
|
+
|
177
|
+
@current_args = args
|
178
|
+
@current_options = opts
|
179
|
+
@invalid_arguments = invalid_options
|
180
|
+
|
181
|
+
@anonymous_command = [ARGV.first, *@anonymized_args].join(' ')
|
182
|
+
begin
|
183
|
+
usage_directory = "#{home_directory}/.turbot/usage"
|
184
|
+
FileUtils.mkdir_p(usage_directory)
|
185
|
+
usage_file = usage_directory << "/#{Turbot::VERSION}"
|
186
|
+
usage = if File.exists?(usage_file)
|
187
|
+
json_decode(File.read(usage_file))
|
188
|
+
else
|
189
|
+
{}
|
190
|
+
end
|
191
|
+
usage[@anonymous_command] ||= 0
|
192
|
+
usage[@anonymous_command] += 1
|
193
|
+
File.write(usage_file, json_encode(usage) + "\n")
|
194
|
+
rescue
|
195
|
+
# usage writing is not important, allow failures
|
196
|
+
end
|
197
|
+
|
198
|
+
if command
|
199
|
+
command_instance = command[:klass].new(args.dup, opts.dup)
|
200
|
+
|
201
|
+
if !@normalized_args.include?('--bot _') && (implied_bot = command_instance.bot rescue nil)
|
202
|
+
@normalized_args << '--bot _'
|
203
|
+
end
|
204
|
+
@normalized_command = [ARGV.first, @normalized_args.sort_by {|arg| arg.gsub('-', '')}].join(' ')
|
205
|
+
|
206
|
+
[ command_instance, command[:method] ]
|
207
|
+
else
|
208
|
+
error([
|
209
|
+
"`#{cmd}` is not a turbot command.",
|
210
|
+
suggestion(cmd, commands.keys + command_aliases.keys),
|
211
|
+
"See `turbot help` for a list of available commands."
|
212
|
+
].compact.join("\n"))
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def self.run(cmd, arguments=[])
|
217
|
+
begin
|
218
|
+
object, method = prepare_run(cmd, arguments.dup)
|
219
|
+
object.send(method)
|
220
|
+
rescue Interrupt, StandardError, SystemExit => error
|
221
|
+
# load likely error classes, as they may not be loaded yet due to defered loads
|
222
|
+
require 'rest_client'
|
223
|
+
raise(error)
|
224
|
+
end
|
225
|
+
rescue Turbot::API::Errors::Unauthorized, RestClient::Unauthorized
|
226
|
+
puts "Authentication failure"
|
227
|
+
if ENV['TURBOT_API_KEY']
|
228
|
+
exit 1
|
229
|
+
else
|
230
|
+
run "login"
|
231
|
+
retry
|
232
|
+
end
|
233
|
+
rescue Turbot::API::Errors::VerificationRequired, RestClient::PaymentRequired => e
|
234
|
+
retry if Turbot::Helpers.confirm_billing
|
235
|
+
rescue Turbot::API::Errors::NotFound => e
|
236
|
+
error extract_error(e.response.body) {
|
237
|
+
e.response.body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found"
|
238
|
+
}
|
239
|
+
rescue RestClient::ResourceNotFound => e
|
240
|
+
error extract_error(e.http_body) {
|
241
|
+
e.http_body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found"
|
242
|
+
}
|
243
|
+
rescue Turbot::API::Errors::Locked => e
|
244
|
+
bot = e.response.headers[:x_confirmation_required]
|
245
|
+
if confirm_command(bot, extract_error(e.response.body))
|
246
|
+
arguments << '--confirm' << bot
|
247
|
+
retry
|
248
|
+
end
|
249
|
+
rescue RestClient::Locked => e
|
250
|
+
bot = e.response.headers[:x_confirmation_required]
|
251
|
+
if confirm_command(bot, extract_error(e.http_body))
|
252
|
+
arguments << '--confirm' << bot
|
253
|
+
retry
|
254
|
+
end
|
255
|
+
rescue Turbot::API::Errors::Timeout, RestClient::RequestTimeout
|
256
|
+
error "API request timed out. Please try again, or contact support@turbot.com if this issue persists."
|
257
|
+
rescue Turbot::API::Errors::ErrorWithResponse => e
|
258
|
+
error extract_error(e.response.body)
|
259
|
+
rescue RestClient::RequestFailed => e
|
260
|
+
error extract_error(e.http_body)
|
261
|
+
rescue CommandFailed => e
|
262
|
+
error e.message
|
263
|
+
rescue OptionParser::ParseError
|
264
|
+
commands[cmd] ? run("help", [cmd]) : run("help")
|
265
|
+
rescue Excon::Errors::SocketError, SocketError => e
|
266
|
+
error("Unable to connect to Turbot API, please check internet connectivity and try again.")
|
267
|
+
ensure
|
268
|
+
display_warnings
|
269
|
+
end
|
270
|
+
|
271
|
+
def self.parse(cmd)
|
272
|
+
commands[cmd] || commands[command_aliases[cmd]]
|
273
|
+
end
|
274
|
+
|
275
|
+
def self.extract_error(body, options={})
|
276
|
+
default_error = block_given? ? yield : "Internal server error.\nRun `turbot status` to check for known platform issues."
|
277
|
+
parse_error_xml(body) || parse_error_json(body) || parse_error_plain(body) || default_error
|
278
|
+
end
|
279
|
+
|
280
|
+
def self.parse_error_xml(body)
|
281
|
+
xml_errors = REXML::Document.new(body).elements.to_a("//errors/error")
|
282
|
+
msg = xml_errors.map { |a| a.text }.join(" / ")
|
283
|
+
return msg unless msg.empty?
|
284
|
+
rescue Exception
|
285
|
+
end
|
286
|
+
|
287
|
+
def self.parse_error_json(body)
|
288
|
+
json = json_decode(body.to_s) rescue false
|
289
|
+
case json
|
290
|
+
when Array
|
291
|
+
json.first.join(' ') # message like [['base', 'message']]
|
292
|
+
when Hash
|
293
|
+
json['error'] || json['error_message'] || json['message'] # message like {'error' => 'message'}
|
294
|
+
else
|
295
|
+
nil
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def self.parse_error_plain(body)
|
300
|
+
return unless body.respond_to?(:headers) && body.headers[:content_type].to_s.include?("text/plain")
|
301
|
+
body.to_s
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "turbot/deprecated"
|
2
|
+
|
3
|
+
module Turbot::Deprecated::Help
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
class HelpGroup < Array
|
9
|
+
attr_reader :title
|
10
|
+
|
11
|
+
def initialize(title)
|
12
|
+
@title = title
|
13
|
+
end
|
14
|
+
|
15
|
+
def command(name, description)
|
16
|
+
self << [name, description]
|
17
|
+
end
|
18
|
+
|
19
|
+
def space
|
20
|
+
self << ['', '']
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
def groups
|
26
|
+
@groups ||= []
|
27
|
+
end
|
28
|
+
|
29
|
+
def group(title, &block)
|
30
|
+
groups << begin
|
31
|
+
group = HelpGroup.new(title)
|
32
|
+
yield group
|
33
|
+
group
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Turbot
|
2
|
+
class API
|
3
|
+
module Errors
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class ErrorWithResponse < Error
|
7
|
+
attr_reader :response
|
8
|
+
|
9
|
+
def initialize(message, response=nil)
|
10
|
+
message = message << "\nbody: #{response.body.inspect}" if response
|
11
|
+
super message
|
12
|
+
@response = response
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Unauthorized < ErrorWithResponse; end
|
17
|
+
class VerificationRequired < ErrorWithResponse; end
|
18
|
+
class Forbidden < ErrorWithResponse; end
|
19
|
+
class NotFound < ErrorWithResponse; end
|
20
|
+
class Timeout < ErrorWithResponse; end
|
21
|
+
class Locked < ErrorWithResponse; end
|
22
|
+
class RateLimitExceeded < ErrorWithResponse; end
|
23
|
+
class RequestFailed < ErrorWithResponse; end
|
24
|
+
class NilApp < ErrorWithResponse; end
|
25
|
+
class MissingManifest < ErrorWithResponse; end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/turbot/excon.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require "turbot/helpers"
|
2
|
+
|
3
|
+
module Turbot::Helpers
|
4
|
+
class LogDisplayer
|
5
|
+
|
6
|
+
include Turbot::Helpers
|
7
|
+
|
8
|
+
attr_reader :api, :bot, :opts
|
9
|
+
|
10
|
+
def initialize(api, bot, opts)
|
11
|
+
@api, @bot, @opts = api, bot, opts
|
12
|
+
end
|
13
|
+
|
14
|
+
def display_logs
|
15
|
+
@assigned_colors = {}
|
16
|
+
@line_start = true
|
17
|
+
@token = nil
|
18
|
+
|
19
|
+
api.read_logs(bot, opts).each do |chunk|
|
20
|
+
unless chunk.empty?
|
21
|
+
if STDOUT.isatty && ENV.has_key?("TERM")
|
22
|
+
display(colorize(chunk))
|
23
|
+
else
|
24
|
+
display(chunk)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
rescue Errno::EPIPE
|
29
|
+
rescue Interrupt => interrupt
|
30
|
+
if STDOUT.isatty && ENV.has_key?("TERM")
|
31
|
+
display("\e[0m")
|
32
|
+
end
|
33
|
+
raise(interrupt)
|
34
|
+
end
|
35
|
+
|
36
|
+
COLORS = %w( cyan yellow green magenta red )
|
37
|
+
COLOR_CODES = {
|
38
|
+
"red" => 31,
|
39
|
+
"green" => 32,
|
40
|
+
"yellow" => 33,
|
41
|
+
"magenta" => 35,
|
42
|
+
"cyan" => 36,
|
43
|
+
}
|
44
|
+
|
45
|
+
def colorize(chunk)
|
46
|
+
lines = []
|
47
|
+
chunk.split("\n").map do |line|
|
48
|
+
if parsed_line = parse_log(line)
|
49
|
+
header, identifier, body = parsed_line
|
50
|
+
@assigned_colors[identifier] ||= COLORS[@assigned_colors.size % COLORS.size]
|
51
|
+
lines << [
|
52
|
+
"\e[#{COLOR_CODES[@assigned_colors[identifier]]}m",
|
53
|
+
header,
|
54
|
+
"\e[0m",
|
55
|
+
body,
|
56
|
+
].join("")
|
57
|
+
elsif not line.empty?
|
58
|
+
lines << line
|
59
|
+
end
|
60
|
+
end
|
61
|
+
lines.join("\n")
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_log(log)
|
65
|
+
return unless parsed = log.match(/^(.*?\[([\w-]+)([\d\.]+)?\]:)(.*)?$/)
|
66
|
+
[1, 2, 4].map { |i| parsed[i] }
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'uri'
|
2
|
+
class PgDumpRestore
|
3
|
+
attr_reader :command
|
4
|
+
|
5
|
+
def initialize(source, target, command)
|
6
|
+
@source = URI.parse(source)
|
7
|
+
@target = URI.parse(target)
|
8
|
+
@command = command
|
9
|
+
|
10
|
+
fill_in_shorthand_uris!
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute
|
14
|
+
prepare
|
15
|
+
run
|
16
|
+
verify
|
17
|
+
end
|
18
|
+
|
19
|
+
def prepare
|
20
|
+
if @target.host == 'localhost'
|
21
|
+
create_local_db
|
22
|
+
else
|
23
|
+
ensure_remote_db_empty
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def verify
|
28
|
+
verify_extensions_match
|
29
|
+
end
|
30
|
+
|
31
|
+
def dump_restore_cmd
|
32
|
+
pg_restore = gen_pg_restore_command(@target)
|
33
|
+
pg_dump = gen_pg_dump_command(@source)
|
34
|
+
"#{pg_dump} | #{pg_restore}"
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def create_local_db
|
40
|
+
dbname = @target.path[1..-1]
|
41
|
+
cdb_output = `createdb #{dbname} 2>&1`
|
42
|
+
if $?.exitstatus != 0
|
43
|
+
if cdb_output =~ /already exists/
|
44
|
+
command.error(cdb_output + "\nPlease drop the local database (`dropdb #{dbname}`) and try again.")
|
45
|
+
else
|
46
|
+
command.error(cdb_output + "\nUnable to create new local database. Ensure your local Postgres is working and try again.")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def ensure_remote_db_empty
|
52
|
+
sql = 'select count(*) = 0 from pg_stat_user_tables;'
|
53
|
+
result = exec_sql_on_uri(sql, @target)
|
54
|
+
unless result == " ?column? \n----------\n t\n(1 row)\n\n"
|
55
|
+
command.error("Remote database is not empty.\nPlease create a new database, or use `turbot pg:reset`")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def gen_pg_dump_command(uri)
|
60
|
+
# It is occasionally necessary to override PGSSLMODE, as when the server
|
61
|
+
# wasn't built to support SSL.
|
62
|
+
%{ env PGPASSWORD=#{uri.password} PGSSLMODE=prefer pg_dump --verbose -F c -Z 0 #{connstring(uri, :skip_d_flag)} }
|
63
|
+
end
|
64
|
+
|
65
|
+
def gen_pg_restore_command(uri)
|
66
|
+
%{ env PGPASSWORD=#{uri.password} pg_restore --verbose --no-acl --no-owner #{connstring(uri)} }
|
67
|
+
end
|
68
|
+
|
69
|
+
def connstring(uri, skip_d_flag=false)
|
70
|
+
database = uri.path[1..-1]
|
71
|
+
user = uri.user ? "-U #{uri.user}" : ""
|
72
|
+
%Q{#{user} -h #{uri.host} -p #{uri.port} #{skip_d_flag ? '' : '-d'} #{database} }
|
73
|
+
end
|
74
|
+
|
75
|
+
def fill_in_shorthand_uris!
|
76
|
+
[@target, @source].each do |uri|
|
77
|
+
uri.host ||= 'localhost'
|
78
|
+
uri.port ||= Integer(ENV['PGPORT'] || 5432)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def verify_extensions_match
|
83
|
+
# It's pretty common for local DBs to not have extensions available that
|
84
|
+
# are used by the remote bot, so take the final precaution of warning if
|
85
|
+
# the extensions available in the local database don't match. We don't
|
86
|
+
# report it if the difference is solely in the version of an extension
|
87
|
+
# used, though.
|
88
|
+
ext_sql = "SELECT extname FROM pg_extension ORDER BY extname;"
|
89
|
+
target_exts = exec_sql_on_uri(ext_sql, @target)
|
90
|
+
source_exts = exec_sql_on_uri(ext_sql, @source)
|
91
|
+
if target_exts != source_exts
|
92
|
+
command.error <<-EOM
|
93
|
+
WARNING: Extensions in newly created target database differ from existing source database.
|
94
|
+
|
95
|
+
Target extensions:
|
96
|
+
#{target_exts}
|
97
|
+
Source extensions:
|
98
|
+
#{source_exts}
|
99
|
+
HINT: You should review output to ensure that any errors
|
100
|
+
ignored are acceptable - entire tables may have been missed, where a dependency
|
101
|
+
could not be resolved. You may need to to install a postgresql-contrib package
|
102
|
+
and retry.
|
103
|
+
EOM
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def exec_sql_on_uri(sql, uri)
|
108
|
+
command.send(:exec_sql_on_uri, sql, uri)
|
109
|
+
end
|
110
|
+
|
111
|
+
def run
|
112
|
+
system dump_restore_cmd
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|