tugboat 2.0.1 → 2.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -3
  3. data/features/{vagrant-adam → tugboat}/config_current_directory.feature +0 -0
  4. data/lib/tugboat/cli.rb +6 -1
  5. data/lib/tugboat/middleware/base.rb +59 -0
  6. data/lib/tugboat/middleware/check_credentials.rb +1 -10
  7. data/lib/tugboat/middleware/find_droplet.rb +23 -18
  8. data/lib/tugboat/middleware/list_droplets.rb +23 -19
  9. data/lib/tugboat/middleware/rebuild_droplet.rb +1 -1
  10. data/lib/tugboat/middleware/snapshot_droplet.rb +1 -1
  11. data/lib/tugboat/middleware/ssh_droplet.rb +15 -2
  12. data/lib/tugboat/middleware/wait_for_state.rb +1 -20
  13. data/lib/tugboat/version.rb +1 -1
  14. data/spec/cli/authorize_cli_spec.rb +2 -2
  15. data/spec/cli/create_cli_spec.rb +10 -10
  16. data/spec/cli/debug_cli_spec.rb +2 -2
  17. data/spec/cli/destroy_cli_spec.rb +22 -6
  18. data/spec/cli/droplets_cli_spec.rb +72 -8
  19. data/spec/cli/env_variable_spec.rb +4 -4
  20. data/spec/cli/halt_cli_spec.rb +24 -4
  21. data/spec/cli/info_cli_spec.rb +55 -10
  22. data/spec/cli/password_reset_cli_spec.rb +20 -4
  23. data/spec/cli/rebuild_cli_spec.rb +59 -19
  24. data/spec/cli/resize_cli_spec.rb +19 -3
  25. data/spec/cli/restart_cli_spec.rb +19 -3
  26. data/spec/cli/snapshot_cli_spec.rb +19 -3
  27. data/spec/cli/ssh_cli_spec.rb +37 -2
  28. data/spec/cli/start_cli_spec.rb +19 -3
  29. data/spec/cli/verify_cli_spec.rb +9 -9
  30. data/spec/cli/wait_cli_spec.rb +19 -3
  31. data/spec/fixtures/401.json +4 -0
  32. data/spec/fixtures/show_droplets.json +0 -4
  33. data/spec/fixtures/show_droplets_paginated_first.json +256 -0
  34. data/spec/fixtures/show_droplets_paginated_last.json +253 -0
  35. data/spec/fixtures/show_droplets_private_ip.json +258 -0
  36. data/spec/middleware/check_credentials_spec.rb +3 -3
  37. data/spec/middleware/ssh_droplet_spec.rb +21 -3
  38. data/tugboat.gemspec +1 -0
  39. metadata +26 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dff182303d9c37521cf816ceb57e0b5b73c9f60f
4
- data.tar.gz: 98b25bcaa9909fcf9dbdc20a23e3db937e77148f
3
+ metadata.gz: 915e7c6438004c7d7e4f44e862b5b25805dfb3ba
4
+ data.tar.gz: 5ff32fa64a881616643aab6c263046b7ddccdcf5
5
5
  SHA512:
6
- metadata.gz: 84eed42cf4f8fbb5927b667bf05f243c34e070c58b9813150c790687fb1e5ed89958644f3c90b789e25e60f4797545f68f6684d7507239ea6fca76c01c9e2ce8
7
- data.tar.gz: 5ccd3c89a919f5a47bd71792effca44afb6c552a1d64d3a085f012cd6846eed8bedb2e191cc961a4570a8b90c33e6af56bfb1a1f76e887beb402016a8df39aff
6
+ metadata.gz: 94024933ca93615895225b97192bf3e6cbd29f960b8168cee9dd7611398a287e276eb3cec308bfebcd65a95b7e74dc91b6f5a20427f296af692c66ec8cca9726
7
+ data.tar.gz: 8c3ceb32fa4a6ffd82e49875646a7914e2aec85e5a4dd75d50eeca1175ed33b811c4857accccb569e15d9e9e9096f49e39dcb75ea36047986286ba46cdf1d303
@@ -2,7 +2,23 @@
2
2
 
3
3
  ## [Unreleased](https://github.com/pearkes/tugboat/tree/HEAD)
4
4
 
5
- [Full Changelog](https://github.com/pearkes/tugboat/compare/v2.0.0...HEAD)
5
+ [Full Changelog](https://github.com/pearkes/tugboat/compare/v2.0.1...HEAD)
6
+
7
+ ## [v2.0.1](https://github.com/pearkes/tugboat/tree/v2.0.1) (2015-11-10)
8
+
9
+ [Full Changelog](https://github.com/pearkes/tugboat/compare/v2.0.0...v2.0.1)
10
+
11
+ **Fixed bugs:**
12
+
13
+ - Slugs don't work [\#204](https://github.com/pearkes/tugboat/issues/204)
14
+
15
+ - New Droplets dont have IP address immediatly, so the info command fails [\#198](https://github.com/pearkes/tugboat/issues/198)
16
+
17
+ **Merged pull requests:**
18
+
19
+ - Fixes format for creating droplets [\#207](https://github.com/pearkes/tugboat/pull/207) ([petems](https://github.com/petems))
20
+
21
+ - Fixes issue with new machines having no network [\#203](https://github.com/pearkes/tugboat/pull/203) ([petems](https://github.com/petems))
6
22
 
7
23
  ## [v2.0.0](https://github.com/pearkes/tugboat/tree/v2.0.0) (2015-11-03)
8
24
 
@@ -38,8 +54,6 @@
38
54
 
39
55
  - tugboat doesn't work with team accounts, because they use api v2 [\#168](https://github.com/pearkes/tugboat/issues/168)
40
56
 
41
- - tugboat ssh not using ssh-agent? [\#160](https://github.com/pearkes/tugboat/issues/160)
42
-
43
57
  **Merged pull requests:**
44
58
 
45
59
  - Fixes multiple keys for droplet creation [\#201](https://github.com/pearkes/tugboat/pull/201) ([petems](https://github.com/petems))
@@ -127,6 +127,10 @@ module Tugboat
127
127
  :type => :string,
128
128
  :aliases => [ "-c", "-y" ,],
129
129
  :desc => "Command to run on the droplet"
130
+ method_option "wait",
131
+ :type => :boolean,
132
+ :aliases => '-w',
133
+ :desc => 'Wait for droplet to become active before trying to SSH'
130
134
  def ssh(name=nil)
131
135
  Middleware.sequence_ssh_droplet.call({
132
136
  "tugboat_action" => __method__,
@@ -138,7 +142,8 @@ module Tugboat
138
142
  "user_droplet_use_private_ip" => options[:use_private_ip],
139
143
  "user_droplet_ssh_opts" => options[:ssh_opts],
140
144
  "user_droplet_ssh_command" => options[:ssh_command],
141
- "user_quiet" => options[:quiet]
145
+ "user_droplet_ssh_wait" => options[:wait],
146
+ "user_quiet" => options[:quiet],
142
147
  })
143
148
  end
144
149
 
@@ -22,7 +22,66 @@ module Tugboat
22
22
  @app.call(env)
23
23
  end
24
24
 
25
+ def verify_credentials(ocean, say_success=false)
26
+ begin
27
+ response = ocean.droplet.all({:per_page =>'1', :page =>'1'})
28
+ rescue Faraday::ClientError => e
29
+ say "Authentication with DigitalOcean failed at an early stage"
30
+ say "Error was: #{e}"
31
+ exit 1
32
+ end
33
+
34
+ unless response.success?
35
+ say "Failed to connect to DigitalOcean. Reason given from API: #{response.id} - #{response.message}", :red
36
+ exit 1
37
+ end
38
+
39
+ say "Authentication with DigitalOcean was successful.", :green if say_success
40
+ end
41
+
42
+ def wait_for_state(droplet_id, desired_state,ocean)
43
+ start_time = Time.now
44
+
45
+ response = ocean.droplet.show droplet_id
46
+
47
+ say ".", nil, false
48
+
49
+ if !response.success?
50
+ say "Failed to get status of Droplet: #{response.message}", :red
51
+ exit 1
52
+ end
53
+
54
+ while response.droplet.status != desired_state do
55
+ sleep 2
56
+ response = ocean.droplet.show droplet_id
57
+ say ".", nil, false
58
+ end
59
+
60
+ total_time = (Time.now - start_time).to_i
61
+
62
+ say "done#{CLEAR} (#{total_time}s)", :green
63
+ end
64
+
65
+ # Get all pages of droplets
66
+ def get_droplet_list(ocean)
67
+
68
+ verify_credentials(ocean)
69
+
70
+ page = ocean.droplet.all(per_page: 200, page: 1)
71
+ if not page.paginated?
72
+ return page.droplets
73
+ end
74
+
75
+ Enumerator.new do |enum|
76
+ page.droplets.each { |drop| enum.yield drop }
77
+ for page_num in 2..page.last_page
78
+ page = ocean.droplet.all(per_page: 200, page: page_num)
79
+ page.droplets.each { |drop| enum.yield drop }
80
+ end
81
+ end
82
+ end
25
83
  end
84
+
26
85
  end
27
86
  end
28
87
 
@@ -7,16 +7,7 @@ module Tugboat
7
7
  def call(env)
8
8
  # We use a harmless API call to check if the authentication will
9
9
  # work.
10
- begin
11
- env['barge'].droplet.all.list
12
- rescue Faraday::Error::ClientError => e
13
- say "Authentication with DigitalOcean failed."
14
- say "Error was: #{e}"
15
- say "Try re-running `tugboat authorize`", :red
16
- exit 1
17
- end
18
-
19
- say "Authentication with DigitalOcean was successful.", :green
10
+ verify_credentials(env['barge'], true)
20
11
 
21
12
  @app.call(env)
22
13
  end
@@ -2,6 +2,19 @@ module Tugboat
2
2
  module Middleware
3
3
  # Check if the client has set-up configuration yet.
4
4
  class FindDroplet < Base
5
+ def get_public_ip(networks)
6
+ get_ip_per_network_type networks, "public"
7
+ end
8
+
9
+ def get_private_ip(networks)
10
+ get_ip_per_network_type networks, "private"
11
+ end
12
+
13
+ def get_ip_per_network_type(networks, type)
14
+ found_network = networks.detect { |n| n.type == type }
15
+ found_network.ip_address if found_network
16
+ end
17
+
5
18
  def call(env)
6
19
  ocean = env['barge']
7
20
  user_fuzzy_name = env['user_droplet_fuzzy_name']
@@ -44,8 +57,8 @@ module Tugboat
44
57
 
45
58
  env["droplet_id"] = response.droplet.id
46
59
  env["droplet_name"] = "(#{response.droplet.name})"
47
- env["droplet_ip"] = response.droplet.ip_address
48
- env["droplet_ip_private"] = response.droplet.private_ip_address
60
+ env["droplet_ip"] = get_public_ip response.droplet.networks.v4
61
+ env["droplet_ip_private"] = get_private_ip response.droplet.networks.v4
49
62
  env["droplet_status"] = response.droplet.status
50
63
  end
51
64
 
@@ -57,12 +70,12 @@ module Tugboat
57
70
  end
58
71
 
59
72
  # Look for the droplet by an exact name match.
60
- ocean.droplet.all.droplets.each do |d|
73
+ (get_droplet_list ocean).each do |d|
61
74
  if d.name == user_droplet_name
62
75
  env["droplet_id"] = d.id
63
76
  env["droplet_name"] = "(#{d.name})"
64
- env["droplet_ip"] = d.ip_address
65
- env["droplet_ip_private"] = d.private_ip_address
77
+ env["droplet_ip"] = get_public_ip d.networks.v4
78
+ env["droplet_ip_private"] = get_private_ip d.networks.v4
66
79
  env["droplet_status"] = d.status
67
80
  end
68
81
  end
@@ -86,7 +99,7 @@ module Tugboat
86
99
  found_droplets = []
87
100
  choices = []
88
101
 
89
- ocean.droplet.all.droplets.each_with_index do |d, i|
102
+ (get_droplet_list ocean).each do |d|
90
103
  # Check to see if one of the droplet names have the fuzzy string.
91
104
  if d.name.upcase.include? user_fuzzy_name.upcase
92
105
  found_droplets << d
@@ -104,9 +117,8 @@ module Tugboat
104
117
  env["droplet_ip"] = '' # No Network Yet
105
118
  env["droplet_ip_private"] = '' # No Network Yet
106
119
  else
107
- env["droplet_ip"] = droplet_return.networks.v4.detect { |address| address.type == 'public' }.ip_address
108
- check_private_ip = droplet_return.networks.v4.detect { |address| address.type == 'private' }
109
- env["droplet_ip_private"] = check_private_ip.ip_address if check_private_ip
120
+ env["droplet_ip"] = get_public_ip droplet_return.networks.v4
121
+ env["droplet_ip_private"] = get_private_ip droplet_return.networks.v4
110
122
  end
111
123
  env["droplet_status"] = droplet_return.status
112
124
  elsif found_droplets.length > 1
@@ -121,17 +133,10 @@ module Tugboat
121
133
  end
122
134
  say
123
135
  choice = ask "Please choose a droplet:", :limited_to => choices
124
- for ip_list in found_droplets[choice.to_i].networks.v4 do
125
- if ip_list.type == "private"
126
- private_ip = ip_list.ip_address
127
- elsif ip_list.type == "public"
128
- public_ip = ip_list.ip_address
129
- end
130
- end
131
136
  env["droplet_id"] = found_droplets[choice.to_i].id
132
137
  env["droplet_name"] = found_droplets[choice.to_i].name
133
- env["droplet_ip"] = public_ip
134
- env["droplet_ip_private"] = private_ip
138
+ env["droplet_ip"] = get_public_ip found_droplets[choice.to_i].networks.v4
139
+ env["droplet_ip_private"] = get_private_ip found_droplets[choice.to_i].networks.v4
135
140
  env["droplet_status"] = found_droplets[choice.to_i].status
136
141
  end
137
142
 
@@ -5,28 +5,32 @@ module Tugboat
5
5
  def call(env)
6
6
  ocean = env['barge']
7
7
 
8
- droplet_list = ocean.droplet.all.droplets
8
+ verify_credentials(ocean)
9
9
 
10
- if droplet_list.empty?
10
+ droplet_list = get_droplet_list ocean
11
+
12
+ has_one = false
13
+ droplet_list.each do |droplet|
14
+ has_one = true
15
+
16
+ private_addr = droplet.networks.v4.detect { |address| address.type == 'private' }
17
+ if private_addr
18
+ private_ip = ", private_ip: #{private_addr.ip_address}"
19
+ end
20
+
21
+ if droplet.status == "active"
22
+ status_color = GREEN
23
+ else
24
+ status_color = RED
25
+ end
26
+
27
+ public_addr = droplet.networks.v4.detect { |address| address.type == 'public' }
28
+ say "#{droplet.name} (ip: #{public_addr.ip_address}#{private_ip}, status: #{status_color}#{droplet.status}#{CLEAR}, region: #{droplet.region.slug}, id: #{droplet.id}#{env["include_urls"] ? droplet_id_to_url(droplet.id) : '' })"
29
+ end
30
+
31
+ if not has_one
11
32
  say "You don't appear to have any droplets.", :red
12
33
  say "Try creating one with #{GREEN}\`tugboat create\`#{CLEAR}"
13
- else
14
- droplet_list.each do |droplet|
15
-
16
- if droplet.private_ip_address
17
- private_addr = droplet.networks.v4.detect { |address| address.type == 'private' }
18
- private_ip = ", privateip: #{private_addr.ip_address}"
19
- end
20
-
21
- if droplet.status == "active"
22
- status_color = GREEN
23
- else
24
- status_color = RED
25
- end
26
-
27
- public_addr = droplet.networks.v4.detect { |address| address.type == 'public' }
28
- say "#{droplet.name} (ip: #{public_addr.ip_address}#{private_ip}, status: #{status_color}#{droplet.status}#{CLEAR}, region: #{droplet.region.slug}, id: #{droplet.id}#{env["include_urls"] ? droplet_id_to_url(droplet.id) : '' })"
29
- end
30
34
  end
31
35
 
32
36
  @app.call(env)
@@ -7,7 +7,7 @@ module Tugboat
7
7
  say "Queuing rebuild for droplet #{env["droplet_id"]} #{env["droplet_name"]} with image #{env["image_id"]} #{env["image_name"]}...", nil, false
8
8
 
9
9
  response = ocean.droplet.rebuild env["droplet_id"],
10
- :image_id => env["image_id"]
10
+ :image => env["image_id"]
11
11
 
12
12
  unless response.success?
13
13
  say "Failed to rebuild Droplet: #{response.message}", :red
@@ -17,7 +17,7 @@ module Tugboat
17
17
  say "Failed to snapshot Droplet: #{response.message}", :red
18
18
  exit 1
19
19
  else
20
- say "Snapshot successful!", :red
20
+ say "Snapshot successful!", :green
21
21
  end
22
22
 
23
23
  @app.call(env)
@@ -5,11 +5,17 @@ module Tugboat
5
5
  say "Executing SSH on Droplet #{env["droplet_name"]}..."
6
6
 
7
7
  options = [
8
- "-o", "IdentitiesOnly=yes",
9
8
  "-o", "LogLevel=ERROR",
10
9
  "-o", "StrictHostKeyChecking=no",
11
10
  "-o", "UserKnownHostsFile=/dev/null",
12
- "-i", File.expand_path(env["config"].ssh_key_path.to_s)]
11
+ ]
12
+
13
+ if env["config"].ssh_key_path.nil? || env["config"].ssh_key_path.empty?
14
+ options.push("-o", "IdentitiesOnly=no")
15
+ else
16
+ options.push("-o", "IdentitiesOnly=yes")
17
+ options.push("-i", File.expand_path(env["config"].ssh_key_path.to_s))
18
+ end
13
19
 
14
20
  if env["user_droplet_ssh_port"]
15
21
  options.push("-p", env["user_droplet_ssh_port"].to_s)
@@ -44,6 +50,11 @@ module Tugboat
44
50
 
45
51
  host_string = "#{ssh_user}@#{host_ip}"
46
52
 
53
+ if env['user_droplet_ssh_wait']
54
+ say "Wait flag given, waiting for droplet to become active"
55
+ wait_for_state(env["droplet_id"],'active',env['barge'])
56
+ end
57
+
47
58
  say "Attempting SSH: #{host_string}"
48
59
 
49
60
  options << host_string
@@ -52,6 +63,8 @@ module Tugboat
52
63
  options.push(env["user_droplet_ssh_command"])
53
64
  end
54
65
 
66
+ say "SShing with options: #{options.join(" ")}"
67
+
55
68
  Kernel.exec("ssh", *options)
56
69
 
57
70
  @app.call(env)
@@ -6,26 +6,7 @@ module Tugboat
6
6
 
7
7
  say "Waiting for droplet to become #{env["user_droplet_desired_state"]}.", nil, false
8
8
 
9
- start_time = Time.now
10
-
11
- response = ocean.droplet.show env["droplet_id"]
12
-
13
- say ".", nil, false
14
-
15
- if !response.success?
16
- say "Failed to get status of Droplet: #{response.message}", :red
17
- exit 1
18
- end
19
-
20
- while response.droplet.status != env["user_droplet_desired_state"] do
21
- sleep 2
22
- response = ocean.droplet.show env["droplet_id"]
23
- say ".", nil, false
24
- end
25
-
26
- total_time = (Time.now - start_time).to_i
27
-
28
- say "done#{CLEAR} (#{total_time}s)", :green
9
+ wait_for_state(env["droplet_id"],env["user_droplet_desired_state"],ocean)
29
10
 
30
11
  @app.call(env)
31
12
  end
@@ -1,3 +1,3 @@
1
1
  module Tugboat
2
- VERSION = "2.0.1"
2
+ VERSION = "2.1.0"
3
3
  end
@@ -11,7 +11,7 @@ describe Tugboat::CLI do
11
11
  end
12
12
 
13
13
  it "asks the right questions and checks credentials" do
14
- stub_request(:get, "https://api.digitalocean.com/v2/droplets?per_page=200").
14
+ stub_request(:get, "https://api.digitalocean.com/v2/droplets?page=1&per_page=1").
15
15
  with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'Bearer foo', 'Content-Type'=>'application/json', 'User-Agent'=>'Faraday v0.9.2'}).
16
16
  to_return(:status => 200, :body => fixture('show_droplets'), :headers => {})
17
17
 
@@ -57,7 +57,7 @@ describe Tugboat::CLI do
57
57
  end
58
58
 
59
59
  it "sets defaults if no input given" do
60
- stub_request(:get, "https://api.digitalocean.com/v2/droplets?per_page=200").
60
+ stub_request(:get, "https://api.digitalocean.com/v2/droplets?page=1&per_page=1").
61
61
  with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>/Bearer/, 'Content-Type'=>'application/json', 'User-Agent'=>'Faraday v0.9.2'}).
62
62
  to_return(:status => 200, :body => fixture('show_droplets'), :headers => {})
63
63
 
@@ -72,8 +72,12 @@ Queueing creation of droplet 'example.com'...Could not find file: /foo/bar/baz.s
72
72
 
73
73
  end
74
74
 
75
- it "doesn't create a droplet when mistyping help command" do
76
- help_text = <<-eos
75
+ context "doesn't create a droplet when mistyping help command" do
76
+
77
+ ['help','--help','-h'].each do |help_attempt|
78
+ it "tugboat create #{help_attempt}" do
79
+
80
+ help_text = <<-eos
77
81
  Usage:
78
82
  rspec create NAME
79
83
 
@@ -91,14 +95,10 @@ Options:
91
95
  Create a droplet.
92
96
  eos
93
97
 
94
- @cli.create('help')
95
- expect($stdout.string).to eq help_text
96
-
97
- @cli.create('--help')
98
- expect($stdout.string).to eq help_text + help_text
99
-
100
- @cli.create('-h')
101
- expect($stdout.string).to eq help_text + help_text + help_text
98
+ @cli.create(help_attempt)
99
+ expect($stdout.string).to eq help_text
100
+ end
101
+ end
102
102
  end
103
103
 
104
104
  it "does not clobber named droplets that contain the word help" do
@@ -12,7 +12,7 @@ describe Tugboat::CLI do
12
12
 
13
13
  it "gives full faraday logs" do
14
14
  pending 'Debug flag not avaliable yet'
15
- stub_request(:get, "https://api.digitalocean.com/v2/droplets?per_page=200").
15
+ stub_request(:get, "https://api.digitalocean.com/v2/droplets?page=1&per_page=200").
16
16
  with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'Bearer foo', 'Content-Type'=>'application/json', 'User-Agent'=>'Faraday v0.9.2'}).
17
17
  to_return(:status => 200, :body => fixture('show_droplets'), :headers => {})
18
18
  @cli.droplets
@@ -28,7 +28,7 @@ stub_request(:get, "https://api.digitalocean.com/v2/droplets?per_page=200").
28
28
 
29
29
  it "gives full faraday logs with redacted API keys" do
30
30
  pending 'Debug flag not avaliable yet'
31
- stub_request(:get, "https://api.digitalocean.com/v2/droplets?per_page=200").
31
+ stub_request(:get, "https://api.digitalocean.com/v2/droplets?page=1&per_page=200").
32
32
  with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'Bearer foo', 'Content-Type'=>'application/json', 'User-Agent'=>'Faraday v0.9.2'}).
33
33
  to_return(:status => 200, :body => fixture('show_droplets'), :headers => {})
34
34
  @cli.droplets