mortar 0.1.0

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.
Files changed (61) hide show
  1. data/README.md +36 -0
  2. data/bin/mortar +13 -0
  3. data/lib/mortar.rb +23 -0
  4. data/lib/mortar/auth.rb +312 -0
  5. data/lib/mortar/cli.rb +54 -0
  6. data/lib/mortar/command.rb +267 -0
  7. data/lib/mortar/command/auth.rb +96 -0
  8. data/lib/mortar/command/base.rb +319 -0
  9. data/lib/mortar/command/clusters.rb +41 -0
  10. data/lib/mortar/command/describe.rb +97 -0
  11. data/lib/mortar/command/generate.rb +121 -0
  12. data/lib/mortar/command/help.rb +166 -0
  13. data/lib/mortar/command/illustrate.rb +97 -0
  14. data/lib/mortar/command/jobs.rb +174 -0
  15. data/lib/mortar/command/pigscripts.rb +45 -0
  16. data/lib/mortar/command/projects.rb +128 -0
  17. data/lib/mortar/command/validate.rb +94 -0
  18. data/lib/mortar/command/version.rb +42 -0
  19. data/lib/mortar/errors.rb +24 -0
  20. data/lib/mortar/generators/generator_base.rb +107 -0
  21. data/lib/mortar/generators/macro_generator.rb +37 -0
  22. data/lib/mortar/generators/pigscript_generator.rb +40 -0
  23. data/lib/mortar/generators/project_generator.rb +67 -0
  24. data/lib/mortar/generators/udf_generator.rb +28 -0
  25. data/lib/mortar/git.rb +233 -0
  26. data/lib/mortar/helpers.rb +488 -0
  27. data/lib/mortar/project.rb +156 -0
  28. data/lib/mortar/snapshot.rb +39 -0
  29. data/lib/mortar/templates/macro/macro.pig +14 -0
  30. data/lib/mortar/templates/pigscript/pigscript.pig +38 -0
  31. data/lib/mortar/templates/pigscript/python_udf.py +13 -0
  32. data/lib/mortar/templates/project/Gemfile +3 -0
  33. data/lib/mortar/templates/project/README.md +8 -0
  34. data/lib/mortar/templates/project/gitignore +4 -0
  35. data/lib/mortar/templates/project/macros/gitkeep +0 -0
  36. data/lib/mortar/templates/project/pigscripts/pigscript.pig +35 -0
  37. data/lib/mortar/templates/project/udfs/python/python_udf.py +13 -0
  38. data/lib/mortar/templates/udf/python_udf.py +13 -0
  39. data/lib/mortar/version.rb +20 -0
  40. data/lib/vendor/mortar/okjson.rb +598 -0
  41. data/lib/vendor/mortar/uuid.rb +312 -0
  42. data/spec/mortar/auth_spec.rb +156 -0
  43. data/spec/mortar/command/auth_spec.rb +46 -0
  44. data/spec/mortar/command/base_spec.rb +82 -0
  45. data/spec/mortar/command/clusters_spec.rb +61 -0
  46. data/spec/mortar/command/describe_spec.rb +135 -0
  47. data/spec/mortar/command/generate_spec.rb +139 -0
  48. data/spec/mortar/command/illustrate_spec.rb +140 -0
  49. data/spec/mortar/command/jobs_spec.rb +364 -0
  50. data/spec/mortar/command/pigscripts_spec.rb +70 -0
  51. data/spec/mortar/command/projects_spec.rb +165 -0
  52. data/spec/mortar/command/validate_spec.rb +119 -0
  53. data/spec/mortar/command_spec.rb +122 -0
  54. data/spec/mortar/git_spec.rb +278 -0
  55. data/spec/mortar/helpers_spec.rb +82 -0
  56. data/spec/mortar/project_spec.rb +76 -0
  57. data/spec/mortar/snapshot_spec.rb +46 -0
  58. data/spec/spec.opts +1 -0
  59. data/spec/spec_helper.rb +278 -0
  60. data/spec/support/display_message_matcher.rb +68 -0
  61. metadata +259 -0
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Mortar CLI
2
+
3
+ The Mortar CLI lets you run Hadoop jobs on the Mortar service.
4
+
5
+ # Setup
6
+
7
+ ## Ruby
8
+
9
+ First, install [rvm](https://rvm.io/rvm/install/).
10
+
11
+ curl -kL https://get.rvm.io | bash -s stable
12
+
13
+ Afterward, add the line recommended by rvm to your bash initialization file.
14
+
15
+ Then, switch to the directory where you've cloned mortar. If you don't have the right version of Ruby installed, you will be prompted to upgrade via rvm.
16
+
17
+ ## Dependencies
18
+
19
+ Install required gems:
20
+
21
+ bundle install
22
+
23
+ # Running
24
+
25
+ You can run the command line through bundle:
26
+
27
+ bundle exec mortar <command> <args>
28
+
29
+ # example
30
+ bundle exec mortar help
31
+
32
+ # Testing
33
+
34
+ To run the tests, do:
35
+
36
+ rake spec
data/bin/mortar ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ # resolve bin path, ignoring symlinks
5
+ require "pathname"
6
+ bin_file = Pathname.new(__FILE__).realpath
7
+
8
+ # add self to libpath
9
+ $:.unshift File.expand_path("../../lib", bin_file)
10
+
11
+ # start up the CLI
12
+ require "mortar/cli"
13
+ Mortar::CLI.start(*ARGV)
data/lib/mortar.rb ADDED
@@ -0,0 +1,23 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require "mortar/version"
18
+
19
+ module Mortar
20
+
21
+ USER_AGENT = "mortar-gem/#{Mortar::VERSION} (#{RUBY_PLATFORM}) ruby/#{RUBY_VERSION}"
22
+
23
+ end
@@ -0,0 +1,312 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Portions of this code from heroku (https://github.com/heroku/heroku/) Copyright Heroku 2008 - 2012,
17
+ # used under an MIT license (https://github.com/heroku/heroku/blob/master/LICENSE).
18
+ #
19
+
20
+ require "mortar"
21
+ require "mortar/helpers"
22
+ require "mortar/errors"
23
+
24
+ require "netrc"
25
+
26
+ class Mortar::Auth
27
+ class << self
28
+ include Mortar::Helpers
29
+
30
+ attr_accessor :credentials
31
+
32
+ def api
33
+ @api ||= begin
34
+ require("mortar-api-ruby")
35
+ api = Mortar::API.new(default_params.merge(:user => user, :api_key => password))
36
+
37
+ def api.request(params, &block)
38
+ response = super
39
+ if response.headers.has_key?('X-Mortar-Warning')
40
+ Mortar::Command.warnings.concat(response.headers['X-Mortar-Warning'].split("\n"))
41
+ end
42
+ response
43
+ end
44
+
45
+ api
46
+ end
47
+ end
48
+
49
+ def login
50
+ delete_credentials
51
+ get_credentials
52
+ end
53
+
54
+ def logout
55
+ delete_credentials
56
+ end
57
+
58
+ def check
59
+ @mortar_user = api.get_user.body
60
+ #Need to ensure user has a github_username
61
+ unless @mortar_user.fetch("user_github_username", nil)
62
+ begin
63
+ ask_for_and_save_github_username
64
+ rescue Mortar::CLI::Errors::InvalidGithubUsername => e
65
+ retry if retry_set_github_username?
66
+ raise e
67
+ end
68
+ end
69
+ end
70
+
71
+ def default_host
72
+ "mortardata.com"
73
+ end
74
+
75
+ def host
76
+ ENV['MORTAR_HOST'] || default_host
77
+ end
78
+
79
+ def reauthorize
80
+ @credentials = ask_for_and_save_credentials
81
+ end
82
+
83
+ def user # :nodoc:
84
+ get_credentials[0]
85
+ end
86
+
87
+ def password # :nodoc:
88
+ get_credentials[1]
89
+ end
90
+
91
+ def api_key(user = get_credentials[0], password = get_credentials[1])
92
+ require("mortar-api-ruby")
93
+ api = Mortar::API.new(default_params)
94
+ api.post_login(user, password).body["api_key"]
95
+ end
96
+
97
+ def get_credentials # :nodoc:
98
+ @credentials ||= (read_credentials || ask_for_and_save_credentials)
99
+ end
100
+
101
+ def delete_credentials
102
+ if netrc
103
+ netrc.delete("api.#{host}")
104
+ netrc.save
105
+ end
106
+ @api, @client, @credentials = nil, nil
107
+ end
108
+
109
+ def netrc_path
110
+ default = Netrc.default_path
111
+ encrypted = default + ".gpg"
112
+ if File.exists?(encrypted)
113
+ encrypted
114
+ else
115
+ default
116
+ end
117
+ end
118
+
119
+ def netrc # :nodoc:
120
+ @netrc ||= begin
121
+ File.exists?(netrc_path) && Netrc.read(netrc_path)
122
+ rescue => error
123
+ if error.message =~ /^Permission bits for/
124
+ perm = File.stat(netrc_path).mode & 0777
125
+ abort("Permissions #{perm} for '#{netrc_path}' are too open. You should run `chmod 0600 #{netrc_path}` so that your credentials are NOT accessible by others.")
126
+ else
127
+ raise error
128
+ end
129
+ end
130
+ end
131
+
132
+ def read_credentials
133
+ if ENV['MORTAR_API_KEY']
134
+ ['', ENV['MORTAR_API_KEY']]
135
+ else
136
+ if netrc
137
+ netrc["api.#{host}"]
138
+ end
139
+ end
140
+ end
141
+
142
+ def write_credentials
143
+ FileUtils.mkdir_p(File.dirname(netrc_path))
144
+ FileUtils.touch(netrc_path)
145
+ unless running_on_windows?
146
+ FileUtils.chmod(0600, netrc_path)
147
+ end
148
+ netrc["api.#{host}"] = self.credentials
149
+ netrc.save
150
+ end
151
+
152
+ def echo_off
153
+ with_tty do
154
+ system "stty -echo"
155
+ end
156
+ end
157
+
158
+ def echo_on
159
+ with_tty do
160
+ system "stty echo"
161
+ end
162
+ end
163
+
164
+ def ask_for_credentials
165
+ puts
166
+ puts "Enter your Mortar credentials."
167
+
168
+ print "Email: "
169
+ user = ask
170
+
171
+ print "Password (typing will be hidden): "
172
+ password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
173
+
174
+ [user, api_key(user, password)]
175
+ end
176
+
177
+ def ask_for_github_username
178
+ puts
179
+ puts "Please enter your github username (not email address)."
180
+
181
+ print "Github Username: "
182
+ github_username = ask
183
+ github_username
184
+ end
185
+
186
+ def ask_for_password_on_windows
187
+ require "Win32API"
188
+ char = nil
189
+ password = ''
190
+
191
+ while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
192
+ break if char == 10 || char == 13 # received carriage return or newline
193
+ if char == 127 || char == 8 # backspace and delete
194
+ password.slice!(-1, 1)
195
+ else
196
+ # windows might throw a -1 at us so make sure to handle RangeError
197
+ (password << char.chr) rescue RangeError
198
+ end
199
+ end
200
+ puts
201
+ return password
202
+ end
203
+
204
+ def ask_for_password
205
+ echo_off
206
+ password = ask
207
+ puts
208
+ echo_on
209
+ return password
210
+ end
211
+
212
+ def ask_for_and_save_credentials
213
+ require("mortar-api-ruby") # for the errors
214
+ begin
215
+ @credentials = ask_for_credentials
216
+ write_credentials
217
+ check
218
+ rescue Mortar::API::Errors::NotFound, Mortar::API::Errors::Unauthorized => e
219
+ delete_credentials
220
+ display "Authentication failed."
221
+ retry if retry_login?
222
+ exit 1
223
+ rescue Mortar::CLI::Errors::InvalidGithubUsername => e
224
+ #Too many failures at setting github username
225
+ display "Authentication failed."
226
+ delete_credentials
227
+ exit 1
228
+ rescue Exception => e
229
+ delete_credentials
230
+ raise e
231
+ end
232
+ # TODO: ensure that keys exist
233
+ #check_for_associated_ssh_key unless Mortar::Command.current_command == "keys:add"
234
+ @credentials
235
+ end
236
+
237
+ def ask_for_and_save_github_username
238
+ require ("mortar-api-ruby")
239
+ begin
240
+ @github_username = ask_for_github_username
241
+ save_github_username
242
+ end
243
+ end
244
+
245
+ def save_github_username
246
+ task_id = api.update_user(@mortar_user['user_id'], {'user_github_username' => @github_username}).body['task_id']
247
+
248
+ task_result = nil
249
+ ticking(polling_interval) do |ticks|
250
+ task_result = api.get_task(task_id).body
251
+ is_finished =
252
+ Mortar::API::Task::STATUSES_COMPLETE.include?(task_result["status_code"])
253
+
254
+ redisplay("Setting github username: %s" %
255
+ [is_finished ? " Done!" : spinner(ticks)],
256
+ is_finished) # only display newline on last message
257
+ if is_finished
258
+ display
259
+ break
260
+ end
261
+ end
262
+
263
+ case task_result['status_code']
264
+ when Mortar::API::Task::STATUS_FAILURE
265
+ error_message = "Setting github username failed with #{task_result['error_type'] || 'error'}"
266
+ error_message += ":\n\n#{task_result['error_message']}\n\n"
267
+ output_with_bang error_message
268
+ raise Mortar::CLI::Errors::InvalidGithubUsername.new
269
+ when Mortar::API::Task::STATUS_SUCCESS
270
+ display "Successfully set github username."
271
+ else
272
+ #Raise error so .netrc file is wiped out.
273
+ raise RuntimeError, "Unknown task status: #{task_result['status_code']}"
274
+ end
275
+ end
276
+
277
+
278
+ def retry_login?
279
+ @login_attempts ||= 0
280
+ @login_attempts += 1
281
+ @login_attempts < 3
282
+ end
283
+
284
+ def retry_set_github_username?
285
+ @set_github_username_attempts ||= 0
286
+ @set_github_username_attempts += 1
287
+ @set_github_username_attempts < 3
288
+ end
289
+
290
+ def polling_interval
291
+ (2.0).to_f
292
+ end
293
+
294
+
295
+ protected
296
+
297
+ def default_params
298
+ full_host = (host =~ /^http/) ? host : "https://api.#{host}"
299
+ verify_ssl = ENV['MORTAR_SSL_VERIFY'] != 'disable' && full_host =~ %r|^https://api.mortardata.com|
300
+ uri = URI(full_host)
301
+ {
302
+ :headers => {
303
+ 'User-Agent' => Mortar::USER_AGENT
304
+ },
305
+ :host => uri.host,
306
+ :port => uri.port.to_s,
307
+ :scheme => uri.scheme,
308
+ :ssl_verify_peer => verify_ssl
309
+ }
310
+ end
311
+ end
312
+ end
data/lib/mortar/cli.rb ADDED
@@ -0,0 +1,54 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Portions of this code from heroku (https://github.com/heroku/heroku/) Copyright Heroku 2008 - 2012,
17
+ # used under an MIT license (https://github.com/heroku/heroku/blob/master/LICENSE).
18
+ #
19
+
20
+ require "mortar"
21
+ require "mortar/command"
22
+ require "mortar/helpers"
23
+
24
+ # workaround for rescue/reraise to define errors in command.rb failing in 1.8.6
25
+ #if RUBY_VERSION =~ /^1.8.6/
26
+ # require('mortar-api')
27
+ # require('rest_client')
28
+ #end
29
+
30
+ class Mortar::CLI
31
+
32
+ extend Mortar::Helpers
33
+
34
+ def self.start(*args)
35
+ begin
36
+ if $stdin.isatty
37
+ $stdin.sync = true
38
+ end
39
+ if $stdout.isatty
40
+ $stdout.sync = true
41
+ end
42
+ command = args.shift.strip rescue "help"
43
+ Mortar::Command.load
44
+ Mortar::Command.run(command, args)
45
+ rescue Interrupt
46
+ `stty icanon echo`
47
+ error("Command cancelled.")
48
+ rescue => error
49
+ styled_error(error)
50
+ exit(1)
51
+ end
52
+ end
53
+
54
+ end
@@ -0,0 +1,267 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Portions of this code from heroku (https://github.com/heroku/heroku/) Copyright Heroku 2008 - 2012,
17
+ # used under an MIT license (https://github.com/heroku/heroku/blob/master/LICENSE).
18
+ #
19
+
20
+ require 'rexml/document'
21
+ require 'mortar/helpers'
22
+ require 'mortar/project'
23
+ require 'mortar/version'
24
+ require 'mortar/api'
25
+ require "optparse"
26
+
27
+
28
+ module Mortar
29
+ module Command
30
+ class CommandFailed < RuntimeError; end
31
+
32
+ extend Mortar::Helpers
33
+
34
+ def self.load
35
+ Dir[File.join(File.dirname(__FILE__), "command", "*.rb")].each do |file|
36
+ require file
37
+ end
38
+ end
39
+
40
+ def self.commands
41
+ @@commands ||= {}
42
+ end
43
+
44
+ def self.command_aliases
45
+ @@command_aliases ||= {}
46
+ end
47
+
48
+ def self.files
49
+ @@files ||= Hash.new {|hash,key| hash[key] = File.readlines(key).map {|line| line.strip}}
50
+ end
51
+
52
+ def self.namespaces
53
+ @@namespaces ||= {}
54
+ end
55
+
56
+ def self.register_command(command)
57
+ commands[command[:command]] = command
58
+ end
59
+
60
+ def self.register_namespace(namespace)
61
+ namespaces[namespace[:name]] = namespace
62
+ end
63
+
64
+ def self.current_command
65
+ @current_command
66
+ end
67
+
68
+ def self.current_command=(new_current_command)
69
+ @current_command = new_current_command
70
+ end
71
+
72
+ def self.current_args
73
+ @current_args
74
+ end
75
+
76
+ def self.current_options
77
+ @current_options ||= {}
78
+ end
79
+
80
+ def self.global_options
81
+ @global_options ||= []
82
+ end
83
+
84
+ def self.invalid_arguments
85
+ @invalid_arguments
86
+ end
87
+
88
+ def self.shift_argument
89
+ # dup argument to get a non-frozen string
90
+ @invalid_arguments.shift.dup rescue nil
91
+ end
92
+
93
+ def self.validate_arguments!
94
+ unless invalid_arguments.empty?
95
+ arguments = invalid_arguments.map {|arg| "\"#{arg}\""}
96
+ if arguments.length == 1
97
+ message = "Invalid argument: #{arguments.first}"
98
+ elsif arguments.length > 1
99
+ message = "Invalid arguments: "
100
+ message << arguments[0...-1].join(", ")
101
+ message << " and "
102
+ message << arguments[-1]
103
+ end
104
+ $stderr.puts(format_with_bang(message))
105
+ run(current_command, ["--help"])
106
+ exit(1)
107
+ end
108
+ end
109
+
110
+ def self.warnings
111
+ @warnings ||= []
112
+ end
113
+
114
+ def self.display_warnings
115
+ unless warnings.empty?
116
+ $stderr.puts(warnings.map {|warning| " ! #{warning}"}.join("\n"))
117
+ end
118
+ end
119
+
120
+ def self.global_option(name, *args, &blk)
121
+ global_options << { :name => name, :args => args, :proc => blk }
122
+ end
123
+
124
+ global_option :help, "--help", "-h"
125
+ global_option :remote, "--remote REMOTE"
126
+ global_option :polling_interval, "--polling_interval SECONDS", "-p"
127
+
128
+ def self.prepare_run(cmd, args=[])
129
+ command = parse(cmd)
130
+
131
+ if args.include?('-h') || args.include?('--help')
132
+ args.unshift(cmd) unless cmd =~ /^-.*/
133
+ cmd = 'help'
134
+ command = parse('help')
135
+ end
136
+
137
+ unless command
138
+ if %w( -v --version ).include?(cmd)
139
+ cmd = 'version'
140
+ command = parse(cmd)
141
+ else
142
+ error([
143
+ "`#{cmd}` is not a mortar command.",
144
+ suggestion(cmd, commands.keys + command_aliases.keys),
145
+ "See `mortar help` for a list of available commands."
146
+ ].compact.join("\n"))
147
+ end
148
+ end
149
+
150
+ @current_command = cmd
151
+
152
+ opts = {}
153
+ invalid_options = []
154
+
155
+ parser = OptionParser.new do |parser|
156
+ # overwrite OptionParsers Officious['version'] to avoid conflicts
157
+ # see: https://github.com/ruby/ruby/blob/trunk/lib/optparse.rb#L814
158
+ parser.on("--version") do |value|
159
+ invalid_options << "--version"
160
+ end
161
+ global_options.each do |global_option|
162
+ parser.on(*global_option[:args]) do |value|
163
+ global_option[:proc].call(value) if global_option[:proc]
164
+ opts[global_option[:name]] = value
165
+ end
166
+ end
167
+ command[:options].each do |name, option|
168
+ parser.on("-#{option[:short]}", "--#{option[:long]}", option[:desc]) do |value|
169
+ opt_name_sym = name.gsub("-", "_").to_sym
170
+ if opts[opt_name_sym]
171
+ # convert multiple instances of an option to an array
172
+ unless opts[opt_name_sym].is_a?(Array)
173
+ opts[opt_name_sym] = [opts[opt_name_sym]]
174
+ end
175
+ opts[opt_name_sym] << value
176
+ else
177
+ opts[opt_name_sym] = value
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ begin
184
+ parser.order!(args) do |nonopt|
185
+ invalid_options << nonopt
186
+ end
187
+ rescue OptionParser::InvalidOption => ex
188
+ invalid_options << ex.args.first
189
+ retry
190
+ end
191
+
192
+ args.concat(invalid_options)
193
+
194
+ @current_args = args
195
+ @current_options = opts
196
+ @invalid_arguments = invalid_options
197
+
198
+ [ command[:klass].new(args.dup, opts.dup), command[:method] ]
199
+ end
200
+
201
+ def self.run(cmd, arguments=[])
202
+ begin
203
+ object, method = prepare_run(cmd, arguments.dup)
204
+ object.send(method)
205
+ rescue Interrupt, StandardError, SystemExit => error
206
+ # load likely error classes, as they may not be loaded yet due to defered loads
207
+ require 'mortar-api-ruby'
208
+ raise(error)
209
+ end
210
+ rescue Mortar::API::Errors::Unauthorized
211
+ puts "Authentication failure"
212
+ unless ENV['MORTAR_API_KEY']
213
+ run "login"
214
+ retry
215
+ end
216
+ rescue Mortar::API::Errors::NotFound => e
217
+ error extract_error(e.response.body) {
218
+ e.response.body =~ /^([\w\s]+ not found).?$/ ? $1 : e.message # "Resource not found"
219
+ }
220
+ rescue Mortar::Project::ProjectError => e
221
+ error e.message
222
+ rescue Mortar::API::Errors::Timeout
223
+ error "API request timed out. Please try again, or contact support@mortardata.com if this issue persists."
224
+ rescue Mortar::API::Errors::ErrorWithResponse => e
225
+ error extract_error(e.response.body)
226
+ rescue CommandFailed => e
227
+ error e.message
228
+ rescue OptionParser::ParseError
229
+ commands[cmd] ? run("help", [cmd]) : run("help")
230
+ ensure
231
+ display_warnings
232
+ end
233
+
234
+ def self.parse(cmd)
235
+ commands[cmd] || commands[command_aliases[cmd]]
236
+ end
237
+
238
+ def self.extract_error(body, options={})
239
+ default_error = block_given? ? yield : "Internal server error."
240
+ parse_error_xml(body) || parse_error_json(body) || parse_error_plain(body) || default_error
241
+ end
242
+
243
+ def self.parse_error_xml(body)
244
+ xml_errors = REXML::Document.new(body).elements.to_a("//errors/error")
245
+ msg = xml_errors.map { |a| a.text }.join(" / ")
246
+ return msg unless msg.empty?
247
+ rescue Exception
248
+ end
249
+
250
+ def self.parse_error_json(body)
251
+ json = json_decode(body.to_s) rescue false
252
+ case json
253
+ when Array
254
+ json.first.last # message like [['base', 'message']]
255
+ when Hash
256
+ json['error'] # message like {'error' => 'message'}
257
+ else
258
+ nil
259
+ end
260
+ end
261
+
262
+ def self.parse_error_plain(body)
263
+ return unless body.respond_to?(:headers) && body.headers[:content_type].to_s.include?("text/plain")
264
+ body.to_s
265
+ end
266
+ end
267
+ end