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.
- 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
|