duostack 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/Rakefile +16 -0
- data/bin/.duostack-console-expect +3 -0
- data/bin/bash/.duostack-console-expect +7 -0
- data/bin/duostack +820 -0
- metadata +71 -0
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
require 'jeweler'
|
5
|
+
Jeweler::Tasks.new do |gem|
|
6
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
7
|
+
gem.name = "duostack"
|
8
|
+
gem.version = `bin/duostack version`.chomp
|
9
|
+
gem.summary = %Q{Duostack command line client}
|
10
|
+
gem.description = %Q{Duostack command line client: create and manage Duostack apps}
|
11
|
+
gem.email = "todd@toddeichel.com"
|
12
|
+
gem.authors = "Todd Eichel"
|
13
|
+
gem.require_paths = ['.'] # default is ["lib"] but we don't have that
|
14
|
+
|
15
|
+
gem.executables = ["duostack", ".duostack-console-expect"]
|
16
|
+
end
|
data/bin/duostack
ADDED
@@ -0,0 +1,820 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#############################################################################
|
4
|
+
# #
|
5
|
+
# Duostack Command Line Client #
|
6
|
+
# #
|
7
|
+
# Copyright © 2011 Duostack, Inc. <http://duostack.com/>. #
|
8
|
+
# #
|
9
|
+
# This program is free software: you can redistribute it and/or modify #
|
10
|
+
# it under the terms of the GNU General Public License as published by #
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or #
|
12
|
+
# (at your option) any later version. #
|
13
|
+
# #
|
14
|
+
# This program is distributed in the hope that it will be useful, #
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
17
|
+
# GNU General Public License for more details. #
|
18
|
+
# #
|
19
|
+
# You should have received a copy of the GNU General Public License #
|
20
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
|
21
|
+
# #
|
22
|
+
#############################################################################
|
23
|
+
|
24
|
+
require 'cgi'
|
25
|
+
$dir = File.dirname(File.expand_path(__FILE__))
|
26
|
+
$client = File.basename(__FILE__)
|
27
|
+
|
28
|
+
module Duostack
|
29
|
+
class Client
|
30
|
+
|
31
|
+
VERSION = '0.1.0'
|
32
|
+
DEPENDENCIES_LAST_MODIFIED = 1297154481
|
33
|
+
DEFAULT_CREDENTIALS_LOCATION = '~/.duostack'
|
34
|
+
USER_AGENT = "duostack-#{VERSION}"
|
35
|
+
|
36
|
+
FLAGS = [ # app is one too but gets special handling
|
37
|
+
'confirm',
|
38
|
+
'long',
|
39
|
+
'skip-dependency-checks'
|
40
|
+
]
|
41
|
+
|
42
|
+
COMMANDS = {
|
43
|
+
:default => 'help', # command used when no other command is given
|
44
|
+
:general => %w(version help), # general commands, can always be run
|
45
|
+
:user => [ # commands requiring credentials (first-time setup)
|
46
|
+
'create',
|
47
|
+
'list',
|
48
|
+
'sync'
|
49
|
+
],
|
50
|
+
:app => [ # commands requiring an app to be specified (either by git inference or flag)
|
51
|
+
'logs',
|
52
|
+
'restart',
|
53
|
+
'ps',
|
54
|
+
'destroy',
|
55
|
+
'console',
|
56
|
+
'rake',
|
57
|
+
'config',
|
58
|
+
'env',
|
59
|
+
'access'
|
60
|
+
],
|
61
|
+
:compound => [ # mult-part commands that expect subsequent arguments, must validate extra args on their own
|
62
|
+
'help',
|
63
|
+
'create',
|
64
|
+
'rake',
|
65
|
+
'config',
|
66
|
+
'env',
|
67
|
+
'access'
|
68
|
+
]
|
69
|
+
}
|
70
|
+
|
71
|
+
def initialize(args=[], client='duostack')
|
72
|
+
@args = args
|
73
|
+
@client = client
|
74
|
+
end
|
75
|
+
|
76
|
+
def run
|
77
|
+
@creds_file = get_flag_arg('creds') || DEFAULT_CREDENTIALS_LOCATION
|
78
|
+
@app_name = get_flag_arg('app') # attempt to get app from args. will also try git repo inference later.
|
79
|
+
@flags = parse_flags
|
80
|
+
@command = @args.shift || COMMANDS[:default]
|
81
|
+
|
82
|
+
# we consider this run to be confirmed if the app name has been specified in the args and the --confirm flag is passed
|
83
|
+
@confirmed = @app_name && @flags.include?('confirm')
|
84
|
+
|
85
|
+
# make sure everything is in order
|
86
|
+
check_dependencies unless @flags.include?('skip-dependency-checks')
|
87
|
+
validate_args # checks for extraneous args and valid command
|
88
|
+
require_app if COMMANDS[:app].include?(@command) # checks that user is set and app can be ID'd
|
89
|
+
require_user if COMMANDS[:user].include?(@command) # checks user credentials
|
90
|
+
|
91
|
+
# everything checks out; time to rock and roll
|
92
|
+
send(@command)
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
# argument processing methods
|
97
|
+
#########################################################################
|
98
|
+
|
99
|
+
# extracts and validates any argument starting with '--', indicating a flag
|
100
|
+
def parse_flags
|
101
|
+
|
102
|
+
# extract flags
|
103
|
+
flags = @args.collect do |arg|
|
104
|
+
arg[2..-1] if arg[0..1] == '--'
|
105
|
+
end.compact
|
106
|
+
|
107
|
+
# remove flags from args
|
108
|
+
@args.delete_if { |arg| arg[0..1] == '--' }
|
109
|
+
|
110
|
+
return flags
|
111
|
+
end
|
112
|
+
|
113
|
+
# ensures that the command and all extracted flags are what we expect, warns if they aren't
|
114
|
+
def validate_args
|
115
|
+
|
116
|
+
invalid = []
|
117
|
+
invalid += [@command] - COMMANDS.values.flatten # validate command
|
118
|
+
invalid += @flags - FLAGS # validate flags
|
119
|
+
|
120
|
+
# unless command is compound (expecting further args), any remaining args must be invalid
|
121
|
+
invalid += @args unless COMMANDS[:compound].include?(@command)
|
122
|
+
|
123
|
+
if invalid.any?
|
124
|
+
exit_with("unrecognized argument: '#{invalid.first}', run '#{@client} help' for usage")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def get_next_args(qty=0, message="missing required argument(s), run '#{@client} help'")
|
129
|
+
next_args = @args.slice!(0, qty)
|
130
|
+
|
131
|
+
# make sure we got as many args as the caller wanted
|
132
|
+
exit_with message unless next_args.length == qty
|
133
|
+
|
134
|
+
# de-arrayify if only one
|
135
|
+
next_args = next_args.first if next_args.length == 1
|
136
|
+
|
137
|
+
return next_args
|
138
|
+
end
|
139
|
+
|
140
|
+
# used to read flags followed by an argument (e.g. --app)
|
141
|
+
# returns the arg following the flag of "name" or nil
|
142
|
+
def get_flag_arg(flag)
|
143
|
+
if flag_index = @args.index("--#{flag}")
|
144
|
+
# slice out flag args so we don't re-use them later, returning second element
|
145
|
+
# will be nil if no arg is passed after --flag; that's okay
|
146
|
+
@args.slice!(flag_index, 2)[1]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
# validation methods
|
152
|
+
#########################################################################
|
153
|
+
|
154
|
+
def check_dependencies
|
155
|
+
|
156
|
+
filename = File.expand_path(@creds_file)
|
157
|
+
return if File.exist?(filename) && File.mtime(filename).to_i > DEPENDENCIES_LAST_MODIFIED
|
158
|
+
|
159
|
+
# ruby
|
160
|
+
if `which ruby`.empty? # how are we even here?
|
161
|
+
exit_with "missing dependency, please install Ruby 1.8.6 or later"
|
162
|
+
end
|
163
|
+
|
164
|
+
# ruby >= 1.8.6
|
165
|
+
if `ruby -v`.split[1].to_f < 1.8
|
166
|
+
exit_with "missing dependency, please install Ruby 1.8.6 or later"
|
167
|
+
end
|
168
|
+
|
169
|
+
# git (any)
|
170
|
+
if `which git`.empty?
|
171
|
+
exit_with "missing dependency, please install Git"
|
172
|
+
end
|
173
|
+
|
174
|
+
# curl
|
175
|
+
if `which curl`.empty?
|
176
|
+
exit_with "missing dependency, please install curl (http://curl.haxx.se/download.html)"
|
177
|
+
end
|
178
|
+
|
179
|
+
# curl SSL
|
180
|
+
# handled inside api_host method
|
181
|
+
|
182
|
+
# expect (only if running console)
|
183
|
+
# TODO: just use ruby expect lib
|
184
|
+
if @command == 'console' && `which expect`.empty?
|
185
|
+
exit_with "missing dependency, please install Expect"
|
186
|
+
end
|
187
|
+
|
188
|
+
# touch .duostack so we know when deps were last checked (but don't create it if it doesn't exist)
|
189
|
+
require 'fileutils'
|
190
|
+
FileUtils.touch(filename) if File.exist?(filename)
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
# ensures the app can be identified from inspecting the git repo or command line args
|
195
|
+
def require_app
|
196
|
+
require_user # all app commands require a user
|
197
|
+
@app_name ||= extract_app_name
|
198
|
+
unless @app_name
|
199
|
+
exit_with "run this command from a Duostack app folder or pass an app name with the --app argument"
|
200
|
+
end
|
201
|
+
return @app_name
|
202
|
+
end
|
203
|
+
|
204
|
+
# ensures user credentials are cached
|
205
|
+
def require_user
|
206
|
+
@credentials ||= `cat #{@creds_file} 2>/dev/null`.chomp
|
207
|
+
|
208
|
+
if @credentials.empty?
|
209
|
+
ssh_key = require_ssh_key # all user commands require an SSH key
|
210
|
+
|
211
|
+
puts "First-time Duostack client setup"
|
212
|
+
print "Email Address: "
|
213
|
+
username = $stdin.gets.chomp
|
214
|
+
password = `bash -c 'read -sp "Password: " passwd; echo $passwd'`.chomp
|
215
|
+
puts '' # clears the line after
|
216
|
+
|
217
|
+
username = CGI::escape(username)
|
218
|
+
password = CGI::escape(password)
|
219
|
+
ssh_key = CGI::escape(ssh_key)
|
220
|
+
|
221
|
+
api_token = api_get('get_token', "api_username=#{username}&api_password=#{password}&ssh_key=#{ssh_key}", 5)
|
222
|
+
`echo 'api_username=#{username}&api_token=#{api_token}' > #{@creds_file}`
|
223
|
+
`chmod 600 #{@creds_file}`
|
224
|
+
@credentials = `cat #{@creds_file} 2>/dev/null`.chomp
|
225
|
+
if @credentials.empty? || @credentials.length < 32 # at least the length of the token
|
226
|
+
exit_with "error saving credentials file, please contact support@duostack.com"
|
227
|
+
end
|
228
|
+
puts "Completed initial setup... waiting for sync..."
|
229
|
+
sleep 4 # TODO: Do a sync check here in the future, 4 secs is safe for now
|
230
|
+
puts ""
|
231
|
+
end
|
232
|
+
|
233
|
+
return @credentials
|
234
|
+
end
|
235
|
+
|
236
|
+
def require_ssh_key
|
237
|
+
@ssh_key ||= `cat #{ssh_key_location}`.chomp
|
238
|
+
unless @ssh_key
|
239
|
+
exit_with "an SSH key is required to run this command, please generate an SSH key and try again (http://docs.duostack.com/command-line-client#setup)"
|
240
|
+
end
|
241
|
+
return @ssh_key
|
242
|
+
end
|
243
|
+
|
244
|
+
def require_confirmation
|
245
|
+
# NOTE: @confirmed is set above in 'run' so we can be assured the app_name came from the --app flag
|
246
|
+
# if assessed confirmation here, the app could have been extracted from the git repo
|
247
|
+
unless @confirmed
|
248
|
+
exit_with "command requires confirmation, run again with '--confirm --app <appname>'"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
# utility methods
|
254
|
+
#########################################################################
|
255
|
+
|
256
|
+
def extract_app_name
|
257
|
+
name = `git remote -v 2>&1 | grep -m1 git@duostack.net | grep ".git" | cut -f 2 -d: | cut -f 1 -d.`.chomp
|
258
|
+
return name.empty? ? nil : name # ensures nil gets returned instead of an empty string
|
259
|
+
end
|
260
|
+
|
261
|
+
def ssh_key_location
|
262
|
+
if `file ~/.ssh/id_rsa.pub`[/text|key/]
|
263
|
+
'~/.ssh/id_rsa.pub'
|
264
|
+
elsif `file ~/.ssh/id_dsa.pub`[/text|key/]
|
265
|
+
'~/.ssh/id_dsa.pub'
|
266
|
+
elsif `file ~/.ssh/identity.pub`[/text|key/]
|
267
|
+
'~/.ssh/identity.pub'
|
268
|
+
else
|
269
|
+
nil
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def api_host
|
274
|
+
@api_host ||= begin
|
275
|
+
host = "https://duostack.duostack.net"
|
276
|
+
if !`curl -V`[/SSL/]
|
277
|
+
warn_with "WARNING! curl SSL support is missing, using insecure plaintext mode"
|
278
|
+
host = "http://duostack.duostack.net"
|
279
|
+
end
|
280
|
+
if local = ENV['DSLOCAL']
|
281
|
+
debug "internal development mode"
|
282
|
+
local ||= "http://localhost:3000"
|
283
|
+
host = local
|
284
|
+
end
|
285
|
+
host
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def api_get(endpoint, params=nil, timeout=20)
|
290
|
+
|
291
|
+
url = "#{api_host}/api/#{endpoint}?#{@credentials}"
|
292
|
+
|
293
|
+
url += "&app_name=#{@app_name}" if @app_name
|
294
|
+
url += "&#{params}" if params
|
295
|
+
|
296
|
+
curl_get(url, timeout)
|
297
|
+
end
|
298
|
+
|
299
|
+
def curl_get(url, timeout=nil)
|
300
|
+
command = "curl -s#{'v' if ENV['DSDEBUG']} -A '#{USER_AGENT}' -w '\n%{http_code}' '#{url}'" # use w flag to append http status code
|
301
|
+
command += " -m #{timeout}" if timeout
|
302
|
+
raw = `#{command}`
|
303
|
+
|
304
|
+
debug command
|
305
|
+
debug raw
|
306
|
+
|
307
|
+
# break apart the raw result and extract the HTTP status code, reassemble
|
308
|
+
parts = raw.split("\n")
|
309
|
+
status = parts.pop.to_i
|
310
|
+
result = parts.join("\n")
|
311
|
+
|
312
|
+
# if the code is 422, we should have a displayable error message, so display directly
|
313
|
+
if status == 422
|
314
|
+
exit_with result
|
315
|
+
end
|
316
|
+
|
317
|
+
case (status / 100) # just get the class of status, e.g. any 4xx code will become 4
|
318
|
+
when 2 # success, return result sans status code
|
319
|
+
return result
|
320
|
+
when 1, 3, 4, 5 # the server is doing something dumb (500 error, redirect, 404s)
|
321
|
+
exit_with "Duostack API error, please try again or contact support@duostack.com"
|
322
|
+
else
|
323
|
+
exit_with "could not connect to Duostack API"
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def debug(message)
|
328
|
+
puts message if ENV['DSDEBUG']
|
329
|
+
end
|
330
|
+
|
331
|
+
def warn_with(message='error')
|
332
|
+
warn "#{@client}: #{message}"
|
333
|
+
end
|
334
|
+
|
335
|
+
def exit_with(message=nil, code=false)
|
336
|
+
warn_with message
|
337
|
+
exit code
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
|
342
|
+
# command methods
|
343
|
+
#########################################################################
|
344
|
+
|
345
|
+
def version
|
346
|
+
puts VERSION
|
347
|
+
end
|
348
|
+
|
349
|
+
def help
|
350
|
+
if content = Help.read_section(@args.shift)
|
351
|
+
puts content
|
352
|
+
else
|
353
|
+
exit_with "unrecognized help section, try '#{@client} help'"
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
def sync
|
358
|
+
# empty method, just makes sure credentials are set
|
359
|
+
end
|
360
|
+
|
361
|
+
def list
|
362
|
+
puts api_get('list_apps')
|
363
|
+
end
|
364
|
+
|
365
|
+
|
366
|
+
def create
|
367
|
+
|
368
|
+
# ensure new app name passed, clean up
|
369
|
+
name = get_next_args(1, "app name is required, try '#{@client} create <appname>'")
|
370
|
+
name = CGI::escape(name.downcase)
|
371
|
+
|
372
|
+
# ensure git repo
|
373
|
+
if `git status 2>&1`[/Not a git/]
|
374
|
+
exit_with "current directory is not a Git repository, run 'git init' first"
|
375
|
+
end
|
376
|
+
|
377
|
+
# ensure no existing duostack remote
|
378
|
+
if extract_app_name
|
379
|
+
exit_with "current directory already initialized for Duostack, remove any Git remotes referencing Duostack first"
|
380
|
+
end
|
381
|
+
|
382
|
+
# create!
|
383
|
+
# TODO: ensure there is not already a remote named duostack
|
384
|
+
puts api_get('create_app', "app_name=#{name}")
|
385
|
+
`git remote add duostack git@duostack.net:#{name}.git 2>/dev/null`
|
386
|
+
puts "Git remote added, to push: 'git push duostack master'"
|
387
|
+
end
|
388
|
+
|
389
|
+
|
390
|
+
def logs
|
391
|
+
puts api_get('get_logs')
|
392
|
+
end
|
393
|
+
|
394
|
+
|
395
|
+
def restart
|
396
|
+
puts api_get('restart')
|
397
|
+
end
|
398
|
+
|
399
|
+
|
400
|
+
def ps
|
401
|
+
puts api_get('get_instances')
|
402
|
+
end
|
403
|
+
|
404
|
+
|
405
|
+
def destroy
|
406
|
+
require_confirmation
|
407
|
+
|
408
|
+
# pull out remote name before we destroy
|
409
|
+
remote = `git remote show duostack 2>/dev/null`
|
410
|
+
|
411
|
+
# destroy!
|
412
|
+
puts api_get("delete_app")
|
413
|
+
|
414
|
+
# attempt to remove duostack git remote
|
415
|
+
# only if "duostack" remote actually references this app's remote
|
416
|
+
if !remote.empty? and remote.scan("git@duostack.net:#{@app_name}.git").length > 0
|
417
|
+
`git remote rm duostack 2>/dev/null`
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
|
422
|
+
def console
|
423
|
+
exec("#{$dir}/.duostack-console-expect #{@app_name}")
|
424
|
+
end
|
425
|
+
|
426
|
+
|
427
|
+
def rake
|
428
|
+
|
429
|
+
# get command(s), if they exist (all remaining args), clean up
|
430
|
+
command = @args.join(' ')
|
431
|
+
command = CGI::escape(command)
|
432
|
+
|
433
|
+
puts api_get('run_rake', "command=#{command}", 60)
|
434
|
+
end
|
435
|
+
|
436
|
+
|
437
|
+
def config
|
438
|
+
|
439
|
+
name, value = @args.slice!(0,2)
|
440
|
+
|
441
|
+
name = CGI::escape(name) if name
|
442
|
+
value = CGI::escape(value) if value
|
443
|
+
|
444
|
+
if name # name provided, get/set config
|
445
|
+
if value # value provided, set config
|
446
|
+
puts api_get('option_set', "name=#{name}&val=#{value}")
|
447
|
+
else # no value provided, get config
|
448
|
+
puts api_get('option_get', "name=#{name}")
|
449
|
+
end
|
450
|
+
else # no name provided, get list
|
451
|
+
puts api_get('option_list')
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
|
456
|
+
def env
|
457
|
+
|
458
|
+
# get command
|
459
|
+
command = @args.shift
|
460
|
+
command ||= 'list' # list is the default
|
461
|
+
|
462
|
+
# ensure command is valid
|
463
|
+
unless %w(add remove rm list ls clear).include?(command)
|
464
|
+
exit_with "invalid argument for 'env', try list, add, remove, or clear"
|
465
|
+
end
|
466
|
+
|
467
|
+
# ensure we have an argument for add/remove commands which require it
|
468
|
+
if %w(add remove rm).include?(command)
|
469
|
+
# gather up and compose subsequent args for add/remove operations
|
470
|
+
# takes all remaining arguments. recompose strings because ruby strips out quotation marks in the args.
|
471
|
+
argument = @args.collect do |arg|
|
472
|
+
if arg.include?('=')
|
473
|
+
result = arg.split('=',2)
|
474
|
+
%Q(#{result[0]}="#{result[1].gsub('"', '\"')}")
|
475
|
+
else
|
476
|
+
arg
|
477
|
+
end
|
478
|
+
end.join(' ')
|
479
|
+
@args.clear # clean up, since we processed every remaining arg
|
480
|
+
|
481
|
+
# warn and exit unless we have an argument to pass
|
482
|
+
if argument.empty?
|
483
|
+
case command
|
484
|
+
when 'add'
|
485
|
+
exit_with "'env add' requires an argument, try 'env add <name>=<value>'"
|
486
|
+
when 'remove', 'rm'
|
487
|
+
exit_with "'env #{command}' requires an argument, try 'env #{command} <name>'"
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
# clean up argument
|
492
|
+
argument = CGI::escape(argument)
|
493
|
+
end
|
494
|
+
|
495
|
+
# finally, process command
|
496
|
+
case command
|
497
|
+
when 'list', 'ls'
|
498
|
+
truncate = !@flags.include?('long')
|
499
|
+
print api_get("list_envs", "truncate=#{truncate}")
|
500
|
+
when 'add'
|
501
|
+
puts api_get("add_env", "input=#{argument}")
|
502
|
+
when 'remove', 'rm'
|
503
|
+
puts api_get("remove_env", "name=#{argument}")
|
504
|
+
when 'clear'
|
505
|
+
require_confirmation
|
506
|
+
puts api_get("clear_envs")
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
|
511
|
+
def access
|
512
|
+
|
513
|
+
# get command
|
514
|
+
command = @args.shift
|
515
|
+
command ||= 'list' # list is the default
|
516
|
+
|
517
|
+
# ensure command is valid
|
518
|
+
unless %w(add list ls).include?(command)
|
519
|
+
exit_with "invalid argument for 'access', try list or add"
|
520
|
+
end
|
521
|
+
|
522
|
+
# ensure we have an argument for add/remove commands which require it
|
523
|
+
if %w(add).include?(command)
|
524
|
+
# gather up and compose remaining args for add/remove operations
|
525
|
+
argument = @args.join(' ')
|
526
|
+
@args.clear # clean up, since we processed every remaining arg
|
527
|
+
|
528
|
+
# warn and exit unless we have an argument to pass
|
529
|
+
if argument.empty?
|
530
|
+
case command
|
531
|
+
when 'add'
|
532
|
+
exit_with "'access add' requires an argument, try 'access add <email>'"
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
# clean up argument
|
537
|
+
argument = CGI::escape(argument)
|
538
|
+
end
|
539
|
+
|
540
|
+
# finally, process command
|
541
|
+
case command
|
542
|
+
when 'list', 'ls'
|
543
|
+
print api_get("list_collabs")
|
544
|
+
when 'add'
|
545
|
+
puts api_get("add_collab", "emails=#{argument}")
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
|
550
|
+
|
551
|
+
module Help
|
552
|
+
|
553
|
+
# available help sections. first will be used as the default.
|
554
|
+
SECTIONS = %w(help create list version logs restart ps destroy config env access console rake)
|
555
|
+
|
556
|
+
class << self
|
557
|
+
|
558
|
+
def read_section(section)
|
559
|
+
|
560
|
+
# use default if not set
|
561
|
+
section ||= SECTIONS.first
|
562
|
+
|
563
|
+
# validate section exists
|
564
|
+
return false unless SECTIONS.include?(section)
|
565
|
+
|
566
|
+
self.send(section).gsub(/\n\s{12}/, "\n") # strips leading spaces so we can keep these indented
|
567
|
+
end
|
568
|
+
|
569
|
+
def help
|
570
|
+
<<-EOF
|
571
|
+
|
572
|
+
Usage: #{$client} <command> [<args>] [--app <appname>]
|
573
|
+
|
574
|
+
App commands must be either run from an app folder (a Git repository with a
|
575
|
+
Duostack remote) or have the app specified with the "--app <appname>" flag. If
|
576
|
+
both are present, the "--app" flag takes precedence.
|
577
|
+
|
578
|
+
The most common commands are listed below.
|
579
|
+
For additional information on any of them, run: #{$client} help <command>
|
580
|
+
|
581
|
+
General Commands:
|
582
|
+
help [<command>] Show this help, or detailed help on <command>
|
583
|
+
create <appname> Initialize a Git repository as a Duostack App
|
584
|
+
list Show all apps associated with your account
|
585
|
+
version Show version of this Duostack client
|
586
|
+
|
587
|
+
App Commands:
|
588
|
+
logs Retrieve server logs
|
589
|
+
restart Restart instances
|
590
|
+
ps List instances with current status
|
591
|
+
destroy Destroy Duostack App and associated data
|
592
|
+
config [<name> [<setting>]] Show or set configuration options
|
593
|
+
env [<operation>] Manage environment variables
|
594
|
+
access [<operation>] Manage app collaborator access
|
595
|
+
|
596
|
+
App Commands - Ruby:
|
597
|
+
console Connect to IRB/Rails console
|
598
|
+
rake [<command>] Run a Rake command
|
599
|
+
|
600
|
+
EOF
|
601
|
+
end
|
602
|
+
|
603
|
+
def create
|
604
|
+
<<-EOF
|
605
|
+
|
606
|
+
Usage: #{$client} create <appname>
|
607
|
+
Example: #{$client} create myappname
|
608
|
+
|
609
|
+
Arguments:
|
610
|
+
<appname> A name for your app (restrictions apply, see below)
|
611
|
+
|
612
|
+
Creates a new Duostack App from the current directory with the given name.
|
613
|
+
|
614
|
+
The current directory must be initialized as a Git repository (run 'git init').
|
615
|
+
Upon running 'create', the app will be created on Duostack and a Git remote with
|
616
|
+
the name 'duostack' will be added to the local repository, which you can push to
|
617
|
+
to deploy your app: git push duostack master.
|
618
|
+
|
619
|
+
The app name must be 4-16 characters long, alphanumeric, and must not start with
|
620
|
+
a number. The use of hyphens, underscores, or any other special characters is
|
621
|
+
not supported.
|
622
|
+
|
623
|
+
EOF
|
624
|
+
end
|
625
|
+
|
626
|
+
def list
|
627
|
+
<<-EOF
|
628
|
+
|
629
|
+
Usage: #{$client} list
|
630
|
+
|
631
|
+
Shows a list of the apps associated with your Duostack account.
|
632
|
+
|
633
|
+
EOF
|
634
|
+
end
|
635
|
+
|
636
|
+
def version
|
637
|
+
<<-EOF
|
638
|
+
|
639
|
+
Usage: #{$client} version
|
640
|
+
|
641
|
+
Displays the client's version number.
|
642
|
+
|
643
|
+
EOF
|
644
|
+
end
|
645
|
+
|
646
|
+
def logs
|
647
|
+
<<-EOF
|
648
|
+
|
649
|
+
Usage: #{$client} logs
|
650
|
+
|
651
|
+
Retreives aggregate logs from all of the app's instances.
|
652
|
+
|
653
|
+
EOF
|
654
|
+
end
|
655
|
+
|
656
|
+
def restart
|
657
|
+
<<-EOF
|
658
|
+
|
659
|
+
Usage: #{$client} restart
|
660
|
+
|
661
|
+
Restarts all of the app's instances. Useful for recovering from errors or
|
662
|
+
forcing a purge of the HTTP cache for your app
|
663
|
+
(see: http://docs.duostack.com/http-caching).
|
664
|
+
|
665
|
+
This will also cause the app to pick up any changes to its configuration options
|
666
|
+
or environment variables made with the 'config' or 'env' commands.
|
667
|
+
|
668
|
+
Apps on Duostack are automatically restarted after each Git push deploy.
|
669
|
+
|
670
|
+
EOF
|
671
|
+
end
|
672
|
+
|
673
|
+
def ps
|
674
|
+
<<-EOF
|
675
|
+
|
676
|
+
Usage: #{$client} ps
|
677
|
+
|
678
|
+
Retreives a listing of all of the app's instances with status information
|
679
|
+
(uptime) for each.
|
680
|
+
|
681
|
+
EOF
|
682
|
+
end
|
683
|
+
|
684
|
+
def destroy
|
685
|
+
<<-EOF
|
686
|
+
|
687
|
+
Usage: #{$client} destroy [--confirm]
|
688
|
+
|
689
|
+
Destroys the app and all associated data. Can not be undone; use with caution.
|
690
|
+
|
691
|
+
Requires confirmation.
|
692
|
+
|
693
|
+
EOF
|
694
|
+
end
|
695
|
+
|
696
|
+
def config
|
697
|
+
<<-EOF
|
698
|
+
|
699
|
+
Usage:
|
700
|
+
#{$client} config lists all configs and current settings
|
701
|
+
#{$client} config <name> shows valid settings for a config
|
702
|
+
#{$client} config <name> <setting> sets a setting for a config
|
703
|
+
|
704
|
+
Examples:
|
705
|
+
#{$client} config lists all configs and current settings
|
706
|
+
#{$client} config stack shows valid settigns for "stack"
|
707
|
+
#{$client} config stack ruby-mri-1.9.2 sets "stack" to "ruby-mri-1.9.2"
|
708
|
+
|
709
|
+
Lists, shows, and sets app config options.
|
710
|
+
|
711
|
+
List Configs: Shows a list of all available config option names with their
|
712
|
+
current settings.
|
713
|
+
|
714
|
+
Show Config: Shows a list of the valid settings for the config option <name>.
|
715
|
+
<name> must be one of the available options shown in the list of configs. The
|
716
|
+
currently selected value is denoted with an asterisk next to it.
|
717
|
+
|
718
|
+
Set Config: Allows setting of config options. Sets config <name> to <setting>.
|
719
|
+
<setting> must be one of the valid settings shown for this config.
|
720
|
+
|
721
|
+
EOF
|
722
|
+
end
|
723
|
+
|
724
|
+
def env
|
725
|
+
<<-EOF
|
726
|
+
|
727
|
+
Usage:
|
728
|
+
#{$client} env [--long] lists all env vars
|
729
|
+
#{$client} env add <name>=<value> adds env var <name> with <value>
|
730
|
+
#{$client} env remove <name> removes env var <name>
|
731
|
+
#{$client} env clear [--confirm] clears all env vars
|
732
|
+
|
733
|
+
Examples:
|
734
|
+
#{$client} env --long lists all env vars
|
735
|
+
#{$client} env add API_KEY=AbO5m5fbrt adds var API_KEY with value AbO5m5fbrt
|
736
|
+
#{$client} env remove API_KEY removes env var API_KEY
|
737
|
+
#{$client} env clear --confirm clears env vars
|
738
|
+
|
739
|
+
Lists, adds, removes, and clears app environment variables.
|
740
|
+
|
741
|
+
List: Lists environment variables currently set on the app. If none are set,
|
742
|
+
output will be blank. List output is abbreviated by default for readability. If
|
743
|
+
you need to see the full values of your environment variables, use the --long
|
744
|
+
flag with this command.
|
745
|
+
|
746
|
+
Add: Add one or more environment variables to the app. Add multiple by
|
747
|
+
supplying additional <name>=<value> pairs separated by spaces. You may need to
|
748
|
+
quote any complex values (with spaces or special characters) that may confuse
|
749
|
+
the client or your shell.
|
750
|
+
|
751
|
+
Remove: Removes one or more environment variables from the app. Remove multiple
|
752
|
+
by supplying additional variable <name> arguments separated by spaces.
|
753
|
+
|
754
|
+
Clear: Clears all environment variables from the app. Requires confirmation.
|
755
|
+
|
756
|
+
EOF
|
757
|
+
end
|
758
|
+
|
759
|
+
def access
|
760
|
+
<<-EOF
|
761
|
+
|
762
|
+
Usage:
|
763
|
+
#{$client} access lists app collaborators
|
764
|
+
#{$client} access add <email> grants access for <email>
|
765
|
+
|
766
|
+
Examples:
|
767
|
+
#{$client} access lists app collaborators
|
768
|
+
#{$client} access add name@example.com grants access for name@example.com
|
769
|
+
|
770
|
+
Lists and adds app collaborator access for users (identified by
|
771
|
+
emails).
|
772
|
+
|
773
|
+
List: Lists collaborator emails who currently have access to the app. If none
|
774
|
+
are set, output will be blank.
|
775
|
+
|
776
|
+
Add: Add one or more collaborators (by email address) to the app. Add multiple
|
777
|
+
by supplying additional <email> arguments separated by spaces.
|
778
|
+
|
779
|
+
EOF
|
780
|
+
end
|
781
|
+
|
782
|
+
def console
|
783
|
+
<<-EOF
|
784
|
+
|
785
|
+
Usage: #{$client} console
|
786
|
+
|
787
|
+
Launches an interactive IRB/console session with your app. You can use this to
|
788
|
+
make any action you would normally make in your app's console.
|
789
|
+
|
790
|
+
Applicable only to Ruby applications.
|
791
|
+
|
792
|
+
EOF
|
793
|
+
end
|
794
|
+
|
795
|
+
def rake
|
796
|
+
<<-EOF
|
797
|
+
|
798
|
+
Usage: #{$client} rake [<command>]
|
799
|
+
|
800
|
+
Runs rake (with an optional <command> argument) on your app. After the task has
|
801
|
+
run, the output will be displayed. Tasks running longer than 60 seconds will not
|
802
|
+
have their full output displayed.
|
803
|
+
|
804
|
+
Passing of environment variables if your rake task requires them is supported
|
805
|
+
(e.g. rake db:seed MODEL=Posts).
|
806
|
+
|
807
|
+
Applicable only to Ruby applications.
|
808
|
+
|
809
|
+
EOF
|
810
|
+
end
|
811
|
+
|
812
|
+
|
813
|
+
end
|
814
|
+
end
|
815
|
+
|
816
|
+
end
|
817
|
+
end
|
818
|
+
|
819
|
+
|
820
|
+
Duostack::Client.new(ARGV.dup, $client).run
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: duostack
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Todd Eichel
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-02-11 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: "Duostack command line client: create and manage Duostack apps"
|
23
|
+
email: todd@toddeichel.com
|
24
|
+
executables:
|
25
|
+
- duostack
|
26
|
+
- .duostack-console-expect
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files: []
|
30
|
+
|
31
|
+
files:
|
32
|
+
- Rakefile
|
33
|
+
- bin/.duostack-console-expect
|
34
|
+
- bin/bash/.duostack-console-expect
|
35
|
+
- bin/duostack
|
36
|
+
has_rdoc: true
|
37
|
+
homepage:
|
38
|
+
licenses: []
|
39
|
+
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
require_paths:
|
44
|
+
- .
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
none: false
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
hash: 3
|
51
|
+
segments:
|
52
|
+
- 0
|
53
|
+
version: "0"
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
requirements: []
|
64
|
+
|
65
|
+
rubyforge_project:
|
66
|
+
rubygems_version: 1.3.7
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: Duostack command line client
|
70
|
+
test_files: []
|
71
|
+
|