duostack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ exec(File.join(File.dirname(__FILE__), 'bash', File.basename(__FILE__)) + ' ' + ARGV[0])
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env expect
2
+ set appname [lindex $argv 0]
3
+
4
+ spawn -noecho ssh cli-console@duostack.net
5
+ expect "Connecting to Ruby console for "
6
+ send "$appname...\n"
7
+ interact
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
+