mortar 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +36 -0
- data/bin/mortar +13 -0
- data/lib/mortar.rb +23 -0
- data/lib/mortar/auth.rb +312 -0
- data/lib/mortar/cli.rb +54 -0
- data/lib/mortar/command.rb +267 -0
- data/lib/mortar/command/auth.rb +96 -0
- data/lib/mortar/command/base.rb +319 -0
- data/lib/mortar/command/clusters.rb +41 -0
- data/lib/mortar/command/describe.rb +97 -0
- data/lib/mortar/command/generate.rb +121 -0
- data/lib/mortar/command/help.rb +166 -0
- data/lib/mortar/command/illustrate.rb +97 -0
- data/lib/mortar/command/jobs.rb +174 -0
- data/lib/mortar/command/pigscripts.rb +45 -0
- data/lib/mortar/command/projects.rb +128 -0
- data/lib/mortar/command/validate.rb +94 -0
- data/lib/mortar/command/version.rb +42 -0
- data/lib/mortar/errors.rb +24 -0
- data/lib/mortar/generators/generator_base.rb +107 -0
- data/lib/mortar/generators/macro_generator.rb +37 -0
- data/lib/mortar/generators/pigscript_generator.rb +40 -0
- data/lib/mortar/generators/project_generator.rb +67 -0
- data/lib/mortar/generators/udf_generator.rb +28 -0
- data/lib/mortar/git.rb +233 -0
- data/lib/mortar/helpers.rb +488 -0
- data/lib/mortar/project.rb +156 -0
- data/lib/mortar/snapshot.rb +39 -0
- data/lib/mortar/templates/macro/macro.pig +14 -0
- data/lib/mortar/templates/pigscript/pigscript.pig +38 -0
- data/lib/mortar/templates/pigscript/python_udf.py +13 -0
- data/lib/mortar/templates/project/Gemfile +3 -0
- data/lib/mortar/templates/project/README.md +8 -0
- data/lib/mortar/templates/project/gitignore +4 -0
- data/lib/mortar/templates/project/macros/gitkeep +0 -0
- data/lib/mortar/templates/project/pigscripts/pigscript.pig +35 -0
- data/lib/mortar/templates/project/udfs/python/python_udf.py +13 -0
- data/lib/mortar/templates/udf/python_udf.py +13 -0
- data/lib/mortar/version.rb +20 -0
- data/lib/vendor/mortar/okjson.rb +598 -0
- data/lib/vendor/mortar/uuid.rb +312 -0
- data/spec/mortar/auth_spec.rb +156 -0
- data/spec/mortar/command/auth_spec.rb +46 -0
- data/spec/mortar/command/base_spec.rb +82 -0
- data/spec/mortar/command/clusters_spec.rb +61 -0
- data/spec/mortar/command/describe_spec.rb +135 -0
- data/spec/mortar/command/generate_spec.rb +139 -0
- data/spec/mortar/command/illustrate_spec.rb +140 -0
- data/spec/mortar/command/jobs_spec.rb +364 -0
- data/spec/mortar/command/pigscripts_spec.rb +70 -0
- data/spec/mortar/command/projects_spec.rb +165 -0
- data/spec/mortar/command/validate_spec.rb +119 -0
- data/spec/mortar/command_spec.rb +122 -0
- data/spec/mortar/git_spec.rb +278 -0
- data/spec/mortar/helpers_spec.rb +82 -0
- data/spec/mortar/project_spec.rb +76 -0
- data/spec/mortar/snapshot_spec.rb +46 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +278 -0
- data/spec/support/display_message_matcher.rb +68 -0
- 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
|
data/lib/mortar/auth.rb
ADDED
@@ -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
|