duostack 0.1.0 → 0.2.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.
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: