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