aptible-cli 0.6.8 → 0.6.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1a3f6b5e9a9f9f325832789f7bbac423de2276c8
4
- data.tar.gz: 328b06f1dcf9cb13c0df4f63f587fb4b79145a57
3
+ metadata.gz: c58858b887b9868e017e93d7248f4e7d1f898dc2
4
+ data.tar.gz: 7b2d52cc1b316205abb7d35c13722a32dccda29a
5
5
  SHA512:
6
- metadata.gz: 8618af8eaf81ca409c8dfd680a00744f265a72da22d87767d679b93c6deadc6d8edf2951fcd488521d68a92ffdbb60f2969a995fb5ad0f3c30afc0ee2858ff48
7
- data.tar.gz: 9da5825e8c2d8e2476ffce6268406f421cc357edd7fc8290801b1b342fe760351082f0f4864e782b113a0a1e8291b3226d815af3b50d23d0438cc0b97106e5d9
6
+ metadata.gz: 2c326bb7a9cc09e2689804e43b0fa00643eb759dbe838f572b0dc44df32caba6f6b76f1a14375e4aa6c1d5cac83457b8a37274c72f14ed843b6afcaa6acb2287
7
+ data.tar.gz: 07bd87aa8c414600d845ce133c868ad1a0df02e9c6518f6202717eabd7f12d8d7b95df42cc5a1f4177bb4f315e6950fc4fa6e13b64059ce2f12f8b0b0a6bf2c3
data/aptible-cli.gemspec CHANGED
@@ -31,4 +31,5 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency 'rake'
32
32
  spec.add_development_dependency 'rspec', '~> 2.0'
33
33
  spec.add_development_dependency 'pry'
34
+ spec.add_development_dependency 'climate_control'
34
35
  end
@@ -8,6 +8,7 @@ require_relative 'helpers/environment'
8
8
  require_relative 'helpers/app'
9
9
  require_relative 'helpers/database'
10
10
  require_relative 'helpers/env'
11
+ require_relative 'helpers/tunnel'
11
12
 
12
13
  require_relative 'subcommands/apps'
13
14
  require_relative 'subcommands/config'
@@ -44,18 +44,6 @@ module Aptible
44
44
  say ''
45
45
  end
46
46
 
47
- def establish_connection(database, local_port)
48
- ENV['ACCESS_TOKEN'] = fetch_token
49
- ENV['APTIBLE_DATABASE'] = database.handle
50
-
51
- remote_port = claim_remote_port(database)
52
- ENV['TUNNEL_PORT'] = remote_port
53
-
54
- tunnel_args = "-L #{local_port}:localhost:#{remote_port}"
55
- command = "ssh #{tunnel_args} #{common_ssh_args(database)}"
56
- Kernel.exec(command)
57
- end
58
-
59
47
  def clone_database(source, dest_handle)
60
48
  op = source.create_operation(type: 'clone', handle: dest_handle)
61
49
  poll_for_success(op)
@@ -63,35 +51,33 @@ module Aptible
63
51
  databases_from_handle(dest_handle, source.account).first
64
52
  end
65
53
 
66
- def dump_database(database)
67
- execute_local_tunnel(database) do |url|
68
- filename = "#{database.handle}.dump"
69
- say "Dumping to #{filename}"
70
- `pg_dump #{url} > #{filename}`
54
+ # Creates a local tunnel and yields the helper
55
+
56
+ def with_local_tunnel(database, port = 0)
57
+ env = {
58
+ 'ACCESS_TOKEN' => fetch_token,
59
+ 'APTIBLE_DATABASE' => database.href
60
+ }
61
+ command = ['ssh', '-q'] + ssh_args(database)
62
+ Helpers::Tunnel.new(env, command).tap do |tunnel_helper|
63
+ tunnel_helper.start(port)
64
+ yield tunnel_helper
65
+ tunnel_helper.stop
71
66
  end
72
67
  end
73
68
 
74
- # Creates a local tunnel and yields the url to it
75
- def execute_local_tunnel(database)
76
- local_port = random_local_port
77
- pid = fork { establish_connection(database, local_port) }
78
-
79
- # TODO: Better test for connection readiness
80
- sleep 10
69
+ # Creates a local PG tunnel and yields the url to it
81
70
 
82
- auth = "aptible:#{database.passphrase}"
83
- host = "localhost:#{local_port}"
84
- yield "postgresql://#{auth}@#{host}/db"
85
- ensure
86
- Process.kill('HUP', pid) if pid
87
- end
71
+ def with_postgres_tunnel(database)
72
+ if database.type != 'postgresql'
73
+ fail Thor::Error, 'This command only works for PostgreSQL'
74
+ end
88
75
 
89
- def random_local_port
90
- # Allocate a dummy server to discover an available port
91
- dummy = TCPServer.new('127.0.0.1', 0)
92
- port = dummy.addr[1]
93
- dummy.close
94
- port
76
+ with_local_tunnel(database) do |tunnel_helper|
77
+ auth = "aptible:#{database.passphrase}"
78
+ host = "localhost:#{tunnel_helper.port}"
79
+ yield "postgresql://#{auth}@#{host}/db"
80
+ end
95
81
  end
96
82
 
97
83
  def local_url(database, local_port)
@@ -102,20 +88,15 @@ module Aptible
102
88
  "127.0.0.1:#{local_port}#{uri.path}"
103
89
  end
104
90
 
105
- def claim_remote_port(database)
106
- ENV['ACCESS_TOKEN'] = fetch_token
107
-
108
- `ssh #{common_ssh_args(database)} 2>/dev/null`.chomp
109
- end
110
-
111
- def common_ssh_args(database)
91
+ def ssh_args(database)
112
92
  host = database.account.bastion_host
113
93
  port = database.account.bastion_port
114
94
 
115
- opts = " -o 'SendEnv=*' -o StrictHostKeyChecking=no " \
116
- '-o UserKnownHostsFile=/dev/null'
117
- connection_args = "-p #{port} root@#{host}"
118
- "#{opts} #{connection_args}"
95
+ ['-o', 'SendEnv=APTIBLE_DATABASE',
96
+ '-o', 'SendEnv=ACCESS_TOKEN',
97
+ '-o', 'StrictHostKeyChecking=no',
98
+ '-o', 'UserKnownHostsFile=/dev/null',
99
+ '-p', port.to_s, "root@#{host}"]
119
100
  end
120
101
  end
121
102
  end
@@ -0,0 +1,76 @@
1
+ require 'socket'
2
+ require 'open3'
3
+
4
+ module Aptible
5
+ module CLI
6
+ module Helpers
7
+ class Tunnel
8
+ def initialize(env, cmd)
9
+ @env = env
10
+ @cmd = cmd
11
+ end
12
+
13
+ def start(desired_port = 0, err_fd = $stderr)
14
+ @local_port = desired_port
15
+ @local_port = random_local_port if @local_port == 0
16
+
17
+ # First, grab a remote port
18
+ out, err, status = Open3.capture3(@env, *@cmd)
19
+ fail "Failed to request remote port: #{err}" unless status.success?
20
+ remote_port = out.chomp
21
+
22
+ # Then, spin up a SSH session using that port and port forwarding
23
+ tunnel_env = @env.merge(
24
+ 'TUNNEL_PORT' => remote_port, # Request a specific port
25
+ 'TUNNEL_SIGNAL_OPEN' => '1' # Request signal when tunnel is up
26
+ )
27
+
28
+ tunnel_cmd = @cmd + [
29
+ '-L', "#{@local_port}:localhost:#{remote_port}",
30
+ '-o', 'SendEnv=TUNNEL_PORT',
31
+ '-o', 'SendEnv=TUNNEL_SIGNAL_OPEN'
32
+ ]
33
+
34
+ r_pipe, w_pipe = IO.pipe
35
+ @pid = Process.spawn(tunnel_env, *tunnel_cmd, in: :close,
36
+ out: w_pipe,
37
+ err: err_fd)
38
+
39
+ # Wait for the tunnel to come up before returning. The other end
40
+ # will send a message on stdout to indicate that the tunnel is ready.
41
+ w_pipe.close
42
+ begin
43
+ r_pipe.readline
44
+ rescue EOFError
45
+ raise 'Server closed the tunnel'
46
+ end
47
+ end
48
+
49
+ def stop
50
+ fail 'You must call #start before calling #stop' if @pid.nil?
51
+ Process.kill('HUP', @pid)
52
+ wait
53
+ end
54
+
55
+ def wait
56
+ Process.wait @pid
57
+ end
58
+
59
+ def port
60
+ fail 'You must call #start before calling #port!' if @local_port.nil?
61
+ @local_port
62
+ end
63
+
64
+ private
65
+
66
+ def random_local_port
67
+ # Allocate a dummy server to discover an available port
68
+ dummy = TCPServer.new('127.0.0.1', 0)
69
+ port = dummy.addr[1]
70
+ dummy.close
71
+ port
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -1,4 +1,5 @@
1
1
  require 'term/ansicolor'
2
+ require 'uri'
2
3
 
3
4
  module Aptible
4
5
  module CLI
@@ -50,15 +51,19 @@ module Aptible
50
51
  option :environment
51
52
  define_method 'db:dump' do |handle|
52
53
  database = ensure_database(options.merge(db: handle))
53
- dump_database(database)
54
+ with_postgres_tunnel(database) do |url|
55
+ filename = "#{handle}.dump"
56
+ say "Dumping to #{filename}"
57
+ `pg_dump #{url} > #{filename}`
58
+ end
54
59
  end
55
60
 
56
61
  desc 'db:execute HANDLE SQL_FILE', 'Executes sql against a database'
57
62
  option :environment
58
63
  define_method 'db:execute' do |handle, sql_path|
59
64
  database = ensure_database(options.merge(db: handle))
60
- execute_local_tunnel(database) do |url|
61
- say "Executing #{sql_path} against #{database.handle}"
65
+ with_postgres_tunnel(database) do |url|
66
+ say "Executing #{sql_path} against #{handle}"
62
67
  `psql #{url} < #{sql_path}`
63
68
  end
64
69
  end
@@ -67,21 +72,31 @@ module Aptible
67
72
  option :environment
68
73
  option :port, type: :numeric
69
74
  define_method 'db:tunnel' do |handle|
75
+ desired_port = Integer(options[:port] || 0)
70
76
  database = ensure_database(options.merge(db: handle))
71
- local_port = options[:port] || random_local_port
72
-
73
77
  say 'Creating tunnel...', :green
74
- say "Connect at #{local_url(database, local_port)}", :green
75
78
 
76
- uri = URI(local_url(database, local_port))
77
- db = uri.path.gsub(%r{^/}, '')
78
- say 'Or, use the following arguments:', :green
79
- say("* Host: #{uri.host}", :green)
80
- say("* Port: #{uri.port}", :green)
81
- say("* Username: #{uri.user}", :green) unless uri.user.empty?
82
- say("* Password: #{uri.password}", :green)
83
- say("* Database: #{db}", :green) unless db.empty?
84
- establish_connection(database, local_port)
79
+ with_local_tunnel(database, desired_port) do |tunnel_helper|
80
+ port = tunnel_helper.port
81
+ say "Connect at #{local_url(database, port)}", :green
82
+
83
+ uri = URI(local_url(database, port))
84
+ db = uri.path.gsub(%r{^/}, '')
85
+ say 'Or, use the following arguments:', :green
86
+ say("* Host: #{uri.host}", :green)
87
+ say("* Port: #{uri.port}", :green)
88
+ say("* Username: #{uri.user}", :green) unless uri.user.empty?
89
+ say("* Password: #{uri.password}", :green)
90
+ say("* Database: #{db}", :green) unless db.empty?
91
+
92
+ say 'Connected. Ctrl-C to close connection.'
93
+
94
+ begin
95
+ tunnel_helper.wait
96
+ rescue Interrupt
97
+ say 'Closing tunnel'
98
+ end
99
+ end
85
100
  end
86
101
 
87
102
  desc 'db:deprovision HANDLE', 'Deprovision a database'
@@ -22,7 +22,7 @@ module Aptible
22
22
  port = app.account.dumptruck_port
23
23
 
24
24
  ENV['ACCESS_TOKEN'] = fetch_token
25
- ENV['APTIBLE_APP'] = app.handle
25
+ ENV['APTIBLE_APP'] = app.href
26
26
  ENV['APTIBLE_CLI_COMMAND'] = 'logs'
27
27
 
28
28
  opts = " -o 'SendEnv=*' -o StrictHostKeyChecking=no " \
@@ -20,7 +20,7 @@ module Aptible
20
20
  port = app.account.dumptruck_port
21
21
 
22
22
  set_env('ACCESS_TOKEN', fetch_token)
23
- set_env('APTIBLE_APP', app.handle)
23
+ set_env('APTIBLE_APP', app.href)
24
24
  set_env('APTIBLE_CLI_COMMAND', 'ps')
25
25
 
26
26
  opts = " -o 'SendEnv=*' -o StrictHostKeyChecking=no " \
@@ -24,7 +24,7 @@ module Aptible
24
24
 
25
25
  ENV['ACCESS_TOKEN'] = fetch_token
26
26
  ENV['APTIBLE_COMMAND'] = command_from_args(*args)
27
- ENV['APTIBLE_APP'] = app.handle
27
+ ENV['APTIBLE_APP'] = app.href
28
28
 
29
29
  opts = options[:force_tty] ? '-t -t' : ''
30
30
  opts << " -o 'SendEnv=*' -o StrictHostKeyChecking=no " \
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.6.8'
3
+ VERSION = '0.6.9'
4
4
  end
5
5
  end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+ require 'climate_control'
3
+
4
+ describe Aptible::CLI::Helpers::Tunnel do
5
+ around do |example|
6
+ mocks_path = File.expand_path('../../../../mock', __FILE__)
7
+ path = "#{mocks_path}:#{ENV['PATH']}"
8
+ ClimateControl.modify PATH: path do
9
+ example.run
10
+ end
11
+ end
12
+
13
+ it 'reuses the port it was given' do
14
+ helper = described_class.new({}, ['ssh_mock.rb'])
15
+
16
+ r, w = IO.pipe
17
+ helper.start(0, w)
18
+ helper.stop
19
+
20
+ expect(r.readline.chomp).to eq('6')
21
+ expect(r.readline.chomp).to eq('-L')
22
+ expect(r.readline.chomp).to match(/\d+:localhost:1234$/)
23
+ expect(r.readline.chomp).to eq('-o')
24
+ expect(r.readline.chomp).to eq('SendEnv=TUNNEL_PORT')
25
+ expect(r.readline.chomp).to eq('-o')
26
+ expect(r.readline.chomp).to eq('SendEnv=TUNNEL_SIGNAL_OPEN')
27
+
28
+ r.close
29
+ w.close
30
+ end
31
+
32
+ it 'accepts a desired port' do
33
+ helper = described_class.new({}, ['ssh_mock.rb'])
34
+ r, w = IO.pipe
35
+ helper.start(5678, w)
36
+ helper.stop
37
+
38
+ expect(r.readline.chomp).to eq('6')
39
+ expect(r.readline.chomp).to eq('-L')
40
+ expect(r.readline.chomp).to eq('5678:localhost:1234')
41
+ expect(r.readline.chomp).to eq('-o')
42
+ expect(r.readline.chomp).to eq('SendEnv=TUNNEL_PORT')
43
+ expect(r.readline.chomp).to eq('-o')
44
+ expect(r.readline.chomp).to eq('SendEnv=TUNNEL_SIGNAL_OPEN')
45
+
46
+ r.close
47
+ w.close
48
+ end
49
+
50
+ it 'captures and displays port discovery errors' do
51
+ helper = described_class.new({ 'FAIL_PORT' => '1' }, ['ssh_mock.rb'])
52
+ expect { helper.start }.to raise_error(/Something went wrong/)
53
+ end
54
+
55
+ it 'captures and displays tunnel errors' do
56
+ helper = described_class.new({ 'FAIL_TUNNEL' => '1' }, ['ssh_mock.rb'])
57
+ expect do
58
+ helper.start(0, File.open(File::NULL, 'w'))
59
+ end.to raise_error(/Server closed the tunnel/)
60
+ end
61
+
62
+ it 'should fail if #port is called before #start' do
63
+ socat = described_class.new({}, [])
64
+ expect { socat.port }.to raise_error(/You must call #start/)
65
+ end
66
+
67
+ it 'should fail if #stop is called before #start' do
68
+ socat = described_class.new({}, [])
69
+ expect { socat.stop }.to raise_error(/You must call #start/)
70
+ end
71
+ end
@@ -7,12 +7,13 @@ end
7
7
  class Account < OpenStruct
8
8
  end
9
9
 
10
+ class SocatHelperMock < OpenStruct
11
+ end
12
+
10
13
  describe Aptible::CLI::Agent do
11
14
  before { subject.stub(:ask) }
12
15
  before { subject.stub(:save_token) }
13
16
  before { subject.stub(:fetch_token) { double 'token' } }
14
- before { subject.stub(:random_local_port) { 4242 } }
15
- before { subject.stub(:establish_connection) }
16
17
 
17
18
  let(:account) do
18
19
  Account.new(bastion_host: 'localhost',
@@ -24,7 +25,14 @@ describe Aptible::CLI::Agent do
24
25
  type: 'postgresql',
25
26
  handle: 'foobar',
26
27
  passphrase: 'password',
27
- connection_url: 'postgresql://aptible:password@10.252.1.125:49158/db'
28
+ connection_url: 'postgresql://aptible:password@10.252.1.125:49158/db',
29
+ account: account
30
+ )
31
+ end
32
+
33
+ let(:socat_helper) do
34
+ SocatHelperMock.new(
35
+ port: 4242
28
36
  )
29
37
  end
30
38
 
@@ -39,11 +47,14 @@ describe Aptible::CLI::Agent do
39
47
  it 'should print a message about how to connect' do
40
48
  allow(Aptible::Api::Database).to receive(:all) { [database] }
41
49
  local_url = 'postgresql://aptible:password@127.0.0.1:4242/db'
50
+
51
+ expect(subject).to receive(:with_local_tunnel).with(database, 0)
52
+ .and_yield(socat_helper)
42
53
  expect(subject).to receive(:say).with('Creating tunnel...', :green)
43
54
  expect(subject).to receive(:say).with("Connect at #{local_url}", :green)
44
55
 
45
56
  # db:tunnel should also explain each component of the URL:
46
- expect(subject).to receive(:say).exactly(6).times
57
+ expect(subject).to receive(:say).exactly(7).times
47
58
  subject.send('db:tunnel', 'foobar')
48
59
  end
49
60
  end
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Emulate server behavior
4
+
5
+ if ENV['TUNNEL_PORT']
6
+ fail 'Something went wrong!' if ENV['FAIL_TUNNEL']
7
+ puts 'TUNNEL READY'
8
+ else
9
+ fail 'Something went wrong!' if ENV['FAIL_PORT']
10
+ puts 1234
11
+ end
12
+
13
+ # Log to stderr so we can collect in test
14
+
15
+ $stderr.puts ARGV.size
16
+ ARGV.each do |a|
17
+ $stderr.puts a
18
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aptible-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.8
4
+ version: 0.6.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Macreery
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-18 00:00:00.000000000 Z
11
+ date: 2016-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aptible-api
@@ -150,6 +150,20 @@ dependencies:
150
150
  - - ">="
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: climate_control
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
153
167
  description: Aptible CLI
154
168
  email:
155
169
  - frank@macreery.com
@@ -176,6 +190,7 @@ files:
176
190
  - lib/aptible/cli/helpers/environment.rb
177
191
  - lib/aptible/cli/helpers/operation.rb
178
192
  - lib/aptible/cli/helpers/token.rb
193
+ - lib/aptible/cli/helpers/tunnel.rb
179
194
  - lib/aptible/cli/subcommands/apps.rb
180
195
  - lib/aptible/cli/subcommands/config.rb
181
196
  - lib/aptible/cli/subcommands/db.rb
@@ -188,11 +203,13 @@ files:
188
203
  - lib/aptible/cli/version.rb
189
204
  - spec/aptible/cli/agent_spec.rb
190
205
  - spec/aptible/cli/helpers/handle_from_git_remote.rb
206
+ - spec/aptible/cli/helpers/tunnel_spec.rb
191
207
  - spec/aptible/cli/subcommands/apps_spec.rb
192
208
  - spec/aptible/cli/subcommands/db_spec.rb
193
209
  - spec/aptible/cli/subcommands/domains_spec.rb
194
210
  - spec/aptible/cli/subcommands/logs_spec.rb
195
211
  - spec/aptible/cli/subcommands/ps_spec.rb
212
+ - spec/mock/ssh_mock.rb
196
213
  - spec/spec_helper.rb
197
214
  homepage: https://github.com/aptible/aptible-cli
198
215
  licenses:
@@ -221,9 +238,11 @@ summary: Command-line interface for Aptible services
221
238
  test_files:
222
239
  - spec/aptible/cli/agent_spec.rb
223
240
  - spec/aptible/cli/helpers/handle_from_git_remote.rb
241
+ - spec/aptible/cli/helpers/tunnel_spec.rb
224
242
  - spec/aptible/cli/subcommands/apps_spec.rb
225
243
  - spec/aptible/cli/subcommands/db_spec.rb
226
244
  - spec/aptible/cli/subcommands/domains_spec.rb
227
245
  - spec/aptible/cli/subcommands/logs_spec.rb
228
246
  - spec/aptible/cli/subcommands/ps_spec.rb
247
+ - spec/mock/ssh_mock.rb
229
248
  - spec/spec_helper.rb