duostack 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/Rakefile +1 -0
  2. data/bin/duostack +244 -135
  3. metadata +5 -5
data/Rakefile CHANGED
@@ -8,6 +8,7 @@ Jeweler::Tasks.new do |gem|
8
8
  gem.version = `bin/duostack version`.chomp
9
9
  gem.summary = %Q{Duostack command line client}
10
10
  gem.description = %Q{Duostack command line client: create and manage Duostack apps}
11
+ gem.homepage = "http://www.duostack.com/"
11
12
  gem.email = "todd@toddeichel.com"
12
13
  gem.authors = "Todd Eichel"
13
14
  gem.require_paths = ['.'] # default is ["lib"] but we don't have that
data/bin/duostack CHANGED
@@ -28,12 +28,16 @@ $client = File.basename(__FILE__)
28
28
  module Duostack
29
29
  class Client
30
30
 
31
- VERSION = '0.1.0'
31
+ VERSION = '0.2.0'
32
32
  DEPENDENCIES_LAST_MODIFIED = 1297154481
33
- DEFAULT_CREDENTIALS_LOCATION = '~/.duostack'
34
33
  USER_AGENT = "duostack-#{VERSION}"
35
34
 
36
- FLAGS = [ # app is one too but gets special handling
35
+ DEFAULTS = {
36
+ :credentials_location => '~/.duostack',
37
+ :remote_name => 'duostack'
38
+ }
39
+
40
+ FLAGS = [ # app and remote get special handling
37
41
  'confirm',
38
42
  'long',
39
43
  'skip-dependency-checks'
@@ -56,7 +60,8 @@ module Duostack
56
60
  'rake',
57
61
  'config',
58
62
  'env',
59
- 'access'
63
+ 'access',
64
+ 'domains'
60
65
  ],
61
66
  :compound => [ # mult-part commands that expect subsequent arguments, must validate extra args on their own
62
67
  'help',
@@ -64,7 +69,8 @@ module Duostack
64
69
  'rake',
65
70
  'config',
66
71
  'env',
67
- 'access'
72
+ 'access',
73
+ 'domains'
68
74
  ]
69
75
  }
70
76
 
@@ -74,8 +80,10 @@ module Duostack
74
80
  end
75
81
 
76
82
  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.
83
+ @creds_file = get_flag_arg('creds') || DEFAULTS[:credentials_location]
84
+ @remote = get_flag_arg('remote') || DEFAULTS[:remote_name]
85
+ # get app from args if we can, otherwise it will be extracted from git remotes during require_app
86
+ @app_name = get_flag_arg('app')
79
87
  @flags = parse_flags
80
88
  @command = @args.shift || COMMANDS[:default]
81
89
 
@@ -179,12 +187,6 @@ module Duostack
179
187
  # curl SSL
180
188
  # handled inside api_host method
181
189
 
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
190
  # touch .duostack so we know when deps were last checked (but don't create it if it doesn't exist)
189
191
  require 'fileutils'
190
192
  FileUtils.touch(filename) if File.exist?(filename)
@@ -194,7 +196,7 @@ module Duostack
194
196
  # ensures the app can be identified from inspecting the git repo or command line args
195
197
  def require_app
196
198
  require_user # all app commands require a user
197
- @app_name ||= extract_app_name
199
+ @app_name ||= app_name_from_git_remotes
198
200
  unless @app_name
199
201
  exit_with "run this command from a Duostack app folder or pass an app name with the --app argument"
200
202
  end
@@ -209,7 +211,7 @@ module Duostack
209
211
  ssh_key = require_ssh_key # all user commands require an SSH key
210
212
 
211
213
  puts "First-time Duostack client setup"
212
- print "Email Address: "
214
+ print "Email: "
213
215
  username = $stdin.gets.chomp
214
216
  password = `bash -c 'read -sp "Password: " passwd; echo $passwd'`.chomp
215
217
  puts '' # clears the line after
@@ -245,7 +247,7 @@ module Duostack
245
247
  # NOTE: @confirmed is set above in 'run' so we can be assured the app_name came from the --app flag
246
248
  # if assessed confirmation here, the app could have been extracted from the git repo
247
249
  unless @confirmed
248
- exit_with "command requires confirmation, run again with '--confirm --app <appname>'"
250
+ exit_with "command requires confirmation, run again with '--confirm --app #{@app_name}'"
249
251
  end
250
252
  end
251
253
 
@@ -253,9 +255,103 @@ module Duostack
253
255
  # utility methods
254
256
  #########################################################################
255
257
 
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
258
+ def process_crud_command(opts)
259
+
260
+ # get action
261
+ action = @args.shift || 'list' # list is the default
262
+ action = 'list' if action == 'ls' # normalize shorthand for list
263
+ action = 'remove' if action == 'rm' # normalize shorthand for remove
264
+
265
+ # extract important things from opts, otherwise merge defaults
266
+ resource = opts[:resource]
267
+ resources = "#{resource}s" # may eventually need a better pluralizer
268
+
269
+ # default settings for opts. priority is:
270
+ # 1. hash under each action (e.g. opts[:actions][:list][:param])
271
+ # 2. general opts (e.g. opts[:param])
272
+ # 3. generic defaults specified here (e.g. `resources`, the pluralized resource name)
273
+ defaults = {
274
+ :error_message => opts[:error_message] || "<#{resource}>",
275
+ :param => opts[:param] || resources,
276
+ :additional_params => opts[:additonal_params] || nil,
277
+ :args_processor => opts[:args_processor] || lambda { |args| args }
278
+ }
279
+
280
+ # actions can be either a hash (containing action-specific options) or an array (accept the defaults)
281
+ if opts[:actions].respond_to?(:keys)
282
+ valid_actions = opts[:actions].keys.map { |x| x.to_s }
283
+ opts = defaults.merge(opts[:actions][action.to_sym])
284
+ else
285
+ valid_actions = opts[:actions]
286
+ opts = defaults.merge(opts)
287
+ end
288
+
289
+
290
+ # ensure action is valid
291
+ unless valid_actions.include?(action)
292
+ exit_with "invalid argument for '#{@command}', try #{sentencize(valid_actions)}"
293
+ end
294
+
295
+ # ensure we have an argument for add/remove actions which require it
296
+ if %w(add remove).include?(action)
297
+ # gather up and compose remaining args for add/remove operations
298
+ argument = opts[:args_processor].call(@args).join(' ')
299
+ @args.clear # clean up, since we processed every remaining arg
300
+
301
+ # warn and exit unless we have an argument to pass
302
+ if argument.empty?
303
+ exit_with "'#{@command} #{action}' requires an argument, try '#{@command} #{action} #{opts[:error_message]}'"
304
+ end
305
+
306
+ # clean up argument
307
+ argument = CGI::escape(argument)
308
+ end
309
+
310
+ # finally, process action
311
+ case action
312
+ when 'list'
313
+ print api_get("list_#{resources}", opts[:additional_params])
314
+ when 'add'
315
+ puts api_get("add_#{resource}", ["#{opts[:param]}=#{argument}", opts[:additional_params]].compact.join('&'))
316
+ when 'remove'
317
+ puts api_get("remove_#{resource}", ["#{opts[:param]}=#{argument}", opts[:additional_params]].compact.join('&'))
318
+ when 'clear'
319
+ require_confirmation
320
+ puts api_get("clear_#{resources}", opts[:additional_params])
321
+ end
322
+ end
323
+
324
+
325
+ # attempts to get the app name out of git remotes
326
+ # if --remote is specified, it will use that assuming it's valid
327
+ # if not, it will look for any remote name that references duostack.net
328
+ def app_name_from_git_remotes
329
+ remotes = `git remote -v 2>/dev/null`.split("\n")
330
+ remotes.reject! { |line| line.split.last == '(fetch)' } # filter out fetch remotes (careful, does not exist in older git versions)
331
+
332
+ # find url of remote of specified name
333
+ valid_remotes = remotes.reject do |line|
334
+ line.split.first != @remote
335
+ end
336
+ remote_url = valid_remotes.first.split[1] unless valid_remotes.empty?
337
+
338
+ # correct url will be in the form "git@duostack.net:appname.git"
339
+ # if remote of specified name is a duostack remote, set app_name
340
+ if !remote_url.to_s.empty? && remote_url[0..16] == "git@duostack.net:" && remote_url[-4..-1] == ".git"
341
+ app_name = remote_url[17..-5]
342
+ else # the specified remote isn't for duostack; look for another (take first match)
343
+ remotes.each do |line|
344
+ result = line.scan(/git@duostack\.net:(\w+)\.git/)
345
+ if result.length > 0 && result[0].length > 0
346
+ app_name = result[0][0]
347
+ remote_name = line.split.first
348
+ warn_with "remote '#{@remote}' does not refer to Duostack, using remote '#{remote_name}' instead"
349
+ break
350
+ end
351
+ end
352
+ end
353
+
354
+ return app_name ||= nil
259
355
  end
260
356
 
261
357
  def ssh_key_location
@@ -324,6 +420,18 @@ module Duostack
324
420
  end
325
421
  end
326
422
 
423
+ def sentencize(array, conjunction='or')
424
+ # http://stackoverflow.com/questions/2038787/join-array-contents-into-an-english-list
425
+ case array.length
426
+ when 0, 1
427
+ array.first.to_s
428
+ when 2
429
+ "#{array.first} #{conjunction} #{array.last}"
430
+ else
431
+ "#{array[0..-2].join(', ')}, #{conjunction} #{array.last}"
432
+ end
433
+ end
434
+
327
435
  def debug(message)
328
436
  puts message if ENV['DSDEBUG']
329
437
  end
@@ -374,16 +482,15 @@ module Duostack
374
482
  exit_with "current directory is not a Git repository, run 'git init' first"
375
483
  end
376
484
 
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"
485
+ # make sure we're not going to step on an existing git remote
486
+ if `git remote`.chomp.split("\n").include?(@remote)
487
+ exit_with "there is already a Git remote named '#{@remote}', please remove it or pass a \ndifferent name with the --remote argument"
380
488
  end
381
489
 
382
- # create!
383
- # TODO: ensure there is not already a remote named duostack
490
+ # create, add remote
384
491
  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'"
492
+ `git remote add #{@remote} git@duostack.net:#{name}.git 2>/dev/null`
493
+ puts "Git remote added, to push: 'git push #{@remote} master'"
387
494
  end
388
495
 
389
496
 
@@ -420,6 +527,13 @@ module Duostack
420
527
 
421
528
 
422
529
  def console
530
+ # TODO: just use ruby expect lib
531
+
532
+ # first check for expect dependency
533
+ if `which expect`.empty?
534
+ exit_with "missing dependency, please install Expect (http://expect.sourceforge.net/)"
535
+ end
536
+
423
537
  exec("#{$dir}/.duostack-console-expect #{@app_name}")
424
538
  end
425
539
 
@@ -454,104 +568,59 @@ module Duostack
454
568
 
455
569
 
456
570
  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
571
+ process_crud_command({
572
+ :resource => 'env',
573
+ :actions => {
574
+ :list => {
575
+ :additional_params => "truncate=#{!@flags.include?('long')}"
576
+ },
577
+ :add => {
578
+ :error_message => '<name>=<value>',
579
+ :param => 'input',
580
+ :args_processor => lambda { |args|
581
+ # recompose strings and quote values because ruby strips out quotation marks in the args.
582
+ args.collect do |arg|
583
+ if arg.include?('=')
584
+ result = arg.split('=',2)
585
+ %Q(#{result[0]}="#{result[1].gsub('"', '\"')}")
586
+ else
587
+ arg
588
+ end
589
+ end
590
+ }
591
+ },
592
+ :remove => {
593
+ :error_message => '<name>',
594
+ :param => 'name'
595
+ },
596
+ :clear => {}
597
+ }
598
+ })
508
599
  end
509
600
 
510
601
 
511
602
  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
603
+ process_crud_command({
604
+ :resource => 'collab',
605
+ :param => 'emails',
606
+ :error_message => '<email>',
607
+ :actions => %w(list add)
608
+ })
547
609
  end
548
610
 
549
611
 
612
+ def domains
613
+ process_crud_command({
614
+ :resource => 'domain',
615
+ :actions => %w(list add remove)
616
+ })
617
+ end
618
+
550
619
 
551
620
  module Help
552
621
 
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)
622
+ # available help sections. first will be used as the default, so unshift 'help' into first position.
623
+ SECTIONS = (COMMANDS[:general] + COMMANDS[:user] + COMMANDS[:app]).unshift('help')
555
624
 
556
625
  class << self
557
626
 
@@ -569,33 +638,39 @@ module Duostack
569
638
  def help
570
639
  <<-EOF
571
640
 
572
- Usage: #{$client} <command> [<args>] [--app <appname>]
641
+ Usage: #{$client} <command> [<args>] [--app <appname>] [--remote <remotename>]
573
642
 
574
643
  App commands must be either run from an app folder (a Git repository with a
575
644
  Duostack remote) or have the app specified with the "--app <appname>" flag. If
576
645
  both are present, the "--app" flag takes precedence.
577
646
 
578
- The most common commands are listed below.
579
- For additional information on any of them, run: #{$client} help <command>
647
+ You can also use the "--remote" flag to set the app based on a specific Git
648
+ remote name. This is common when you run two Duostack Apps (e.g. staging and
649
+ production) from the same folder. This flag can also be used when running
650
+ "create" to have the client set up a custom remote name (default is "duostack").
651
+
652
+ The most common commands are listed below. For additional information on any of
653
+ them, run: #{$client} help <command>
580
654
 
581
655
  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
656
+ help [<command>] Show this help, or detailed help on <command>
657
+ create <appname> Initialize a Git repository as a Duostack App
658
+ list Show all apps associated with your account
659
+ version Show version of this Duostack client
586
660
 
587
661
  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
662
+ logs Retrieve server logs
663
+ restart Restart instances
664
+ ps List instances with current status
665
+ destroy Destroy Duostack App and associated data
666
+ config [<name> [<setting>]] Show or set configuration options
667
+ env [<operation>] Manage environment variables
668
+ access [<operation>] Manage app collaborator access
669
+ domains [<operation>] Manage custom domains
595
670
 
596
671
  App Commands - Ruby:
597
- console Connect to IRB/Rails console
598
- rake [<command>] Run a Rake command
672
+ console Connect to IRB/Rails console
673
+ rake [<command>] Run a Rake command
599
674
 
600
675
  EOF
601
676
  end
@@ -603,18 +678,25 @@ module Duostack
603
678
  def create
604
679
  <<-EOF
605
680
 
606
- Usage: #{$client} create <appname>
607
- Example: #{$client} create myappname
681
+ Usage:
682
+ #{$client} create <appname>
683
+ #{$client} create <appname> --remote <remotename>
684
+
685
+ Example:
686
+ #{$client} create myappname
687
+ #{$client} create myappname --remote staging
608
688
 
609
689
  Arguments:
610
690
  <appname> A name for your app (restrictions apply, see below)
691
+ <remotename> An optional name for the created Git remote
611
692
 
612
693
  Creates a new Duostack App from the current directory with the given name.
613
694
 
614
695
  The current directory must be initialized as a Git repository (run 'git init').
615
696
  Upon running 'create', the app will be created on Duostack and a Git remote with
616
697
  the name 'duostack' will be added to the local repository, which you can push to
617
- to deploy your app: git push duostack master.
698
+ to deploy your app: git push duostack master. If a custom remote name is
699
+ specified with the '--remote' flag, that will be used instead.
618
700
 
619
701
  The app name must be 4-16 characters long, alphanumeric, and must not start with
620
702
  a number. The use of hyphens, underscores, or any other special characters is
@@ -767,8 +849,7 @@ module Duostack
767
849
  #{$client} access lists app collaborators
768
850
  #{$client} access add name@example.com grants access for name@example.com
769
851
 
770
- Lists and adds app collaborator access for users (identified by
771
- emails).
852
+ Lists and adds app collaborator access for users (identified by emails).
772
853
 
773
854
  List: Lists collaborator emails who currently have access to the app. If none
774
855
  are set, output will be blank.
@@ -779,6 +860,34 @@ module Duostack
779
860
  EOF
780
861
  end
781
862
 
863
+ def domains
864
+ <<-EOF
865
+
866
+ Usage:
867
+ #{$client} domains lists all custom domains
868
+ #{$client} domains add <domain> adds <domain> as a custom domain
869
+ #{$client} domains remove <domain> removes custom domain <domain>
870
+
871
+ Examples:
872
+ #{$client} domains lists all env vars
873
+ #{$client} domains add app.example.com adds app.example.com to app
874
+ #{$client} domains remove app.example.com removes app.example.com from app
875
+
876
+ Lists, adds, and removes custom domains
877
+ (http://docs.duostack.com/custom-domains).
878
+
879
+ List: Lists custom domains currently set on the app. If none are set, output
880
+ will be blank.
881
+
882
+ Add: Add one or more custom domains to the app. Add multiple by supplying
883
+ additional <domain> arguments separated by spaces.
884
+
885
+ Remove: Removes one or more custom domains from the app. Remove multiple by
886
+ supplying additional <domain> arguments separated by spaces.
887
+
888
+ EOF
889
+ end
890
+
782
891
  def console
783
892
  <<-EOF
784
893
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duostack
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
8
+ - 2
9
9
  - 0
10
- version: 0.1.0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Todd Eichel
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-02-11 00:00:00 -05:00
18
+ date: 2011-02-17 00:00:00 -05:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
@@ -34,7 +34,7 @@ files:
34
34
  - bin/bash/.duostack-console-expect
35
35
  - bin/duostack
36
36
  has_rdoc: true
37
- homepage:
37
+ homepage: http://www.duostack.com/
38
38
  licenses: []
39
39
 
40
40
  post_install_message: