turbot 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +15 -0
  2. data/README.md +36 -0
  3. data/bin/turbot +17 -0
  4. data/data/cacert.pem +3988 -0
  5. data/lib/turbot/auth.rb +315 -0
  6. data/lib/turbot/cli.rb +38 -0
  7. data/lib/turbot/client/cisaurus.rb +25 -0
  8. data/lib/turbot/client/pgbackups.rb +113 -0
  9. data/lib/turbot/client/rendezvous.rb +111 -0
  10. data/lib/turbot/client/ssl_endpoint.rb +25 -0
  11. data/lib/turbot/client/turbot_postgresql.rb +148 -0
  12. data/lib/turbot/client.rb +757 -0
  13. data/lib/turbot/command/auth.rb +85 -0
  14. data/lib/turbot/command/base.rb +192 -0
  15. data/lib/turbot/command/bots.rb +326 -0
  16. data/lib/turbot/command/config.rb +123 -0
  17. data/lib/turbot/command/help.rb +179 -0
  18. data/lib/turbot/command/keys.rb +115 -0
  19. data/lib/turbot/command/logs.rb +34 -0
  20. data/lib/turbot/command/ssl.rb +43 -0
  21. data/lib/turbot/command/status.rb +51 -0
  22. data/lib/turbot/command/update.rb +47 -0
  23. data/lib/turbot/command/version.rb +23 -0
  24. data/lib/turbot/command.rb +304 -0
  25. data/lib/turbot/deprecated/help.rb +38 -0
  26. data/lib/turbot/deprecated.rb +5 -0
  27. data/lib/turbot/distribution.rb +9 -0
  28. data/lib/turbot/errors.rb +28 -0
  29. data/lib/turbot/excon.rb +11 -0
  30. data/lib/turbot/helpers/log_displayer.rb +70 -0
  31. data/lib/turbot/helpers/pg_dump_restore.rb +115 -0
  32. data/lib/turbot/helpers/turbot_postgresql.rb +213 -0
  33. data/lib/turbot/helpers.rb +521 -0
  34. data/lib/turbot/plugin.rb +165 -0
  35. data/lib/turbot/updater.rb +171 -0
  36. data/lib/turbot/version.rb +3 -0
  37. data/lib/turbot.rb +19 -0
  38. data/lib/vendor/turbot/okjson.rb +598 -0
  39. data/spec/helper/legacy_help.rb +16 -0
  40. data/spec/helper/pg_dump_restore_spec.rb +67 -0
  41. data/spec/schemas/dummy_schema.json +12 -0
  42. data/spec/spec.opts +1 -0
  43. data/spec/spec_helper.rb +220 -0
  44. data/spec/support/display_message_matcher.rb +49 -0
  45. data/spec/support/dummy_api.rb +120 -0
  46. data/spec/support/openssl_mock_helper.rb +8 -0
  47. data/spec/support/organizations_mock_helper.rb +11 -0
  48. data/spec/turbot/auth_spec.rb +214 -0
  49. data/spec/turbot/client/pgbackups_spec.rb +43 -0
  50. data/spec/turbot/client/rendezvous_spec.rb +62 -0
  51. data/spec/turbot/client/ssl_endpoint_spec.rb +48 -0
  52. data/spec/turbot/client/turbot_postgresql_spec.rb +71 -0
  53. data/spec/turbot/client_spec.rb +548 -0
  54. data/spec/turbot/command/auth_spec.rb +38 -0
  55. data/spec/turbot/command/base_spec.rb +66 -0
  56. data/spec/turbot/command/bots_spec.rb +54 -0
  57. data/spec/turbot/command/config_spec.rb +143 -0
  58. data/spec/turbot/command/help_spec.rb +90 -0
  59. data/spec/turbot/command/keys_spec.rb +117 -0
  60. data/spec/turbot/command/logs_spec.rb +60 -0
  61. data/spec/turbot/command/status_spec.rb +48 -0
  62. data/spec/turbot/command/version_spec.rb +16 -0
  63. data/spec/turbot/command_spec.rb +131 -0
  64. data/spec/turbot/helpers/turbot_postgresql_spec.rb +181 -0
  65. data/spec/turbot/helpers_spec.rb +48 -0
  66. data/spec/turbot/plugin_spec.rb +172 -0
  67. data/spec/turbot/updater_spec.rb +44 -0
  68. data/templates/manifest.json +7 -0
  69. data/templates/scraper.py +5 -0
  70. data/templates/scraper.rb +6 -0
  71. 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,5 @@
1
+ require "turbot"
2
+
3
+ module Turbot::Deprecated
4
+ end
5
+
@@ -0,0 +1,9 @@
1
+ module Turbot
2
+ module Distribution
3
+ def self.files
4
+ Dir[File.expand_path("../../../{bin,data,lib}/**/*", __FILE__)].select do |file|
5
+ File.file?(file)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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
@@ -0,0 +1,11 @@
1
+ module Excon
2
+
3
+ def self.get_with_redirect(url, options={})
4
+ res = Excon.get(url, options)
5
+ if [301, 302].include?(res.status)
6
+ return self.get_with_redirect(res.headers["Location"], options)
7
+ end
8
+ res
9
+ end
10
+
11
+ end
@@ -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
+