mortar 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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