turbot 0.0.2
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.
- 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
|
+
|