aptible-cli 0.8.3 → 0.8.4

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: 15ef26a1d8174bd57b27355dac98734659336277
4
- data.tar.gz: 5521c5143aa5b261a2a929d24f5933aceb339711
3
+ metadata.gz: cd702a7afbcc8290e375387617c228b388b69918
4
+ data.tar.gz: bbbcb95f4ebc9deac6671049c7534c83e2127d1e
5
5
  SHA512:
6
- metadata.gz: 2cedfbebbf101cac17f840d8701cf5198238c8081dc09059aaa87e8f78f4963530441090855adb62a96cdf411ed6ed24197279db96ede0dde7cccc50bc89325f
7
- data.tar.gz: a8a342ebd3e1c1f6512dbe9c1d5f9cb86382dc8ab1f49bd589f5dabacadaa2bac1ce8b5c81bc1568dcf98f3b6f5c0a0ff971b8a18d2462dfad7c0a90e7b1c623
6
+ metadata.gz: c975747091a4d3232c190baee45865bd38dcde4fe8c585ac78a3b86050f5e382432ed78e719e6fb3ae947fd5e1f213e765ff336d3e07886d97b26feb4ae6fae9
7
+ data.tar.gz: e2b3152c9a2e6a9398b8dc87031b7791b7ad614836d9a548fa9721db3b7864101f53734cf55701940e8c9cd0db93c73b6fbd0ff694c1a7a5f8e0d6733ee02fac
data/aptible-cli.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.test_files = spec.files.grep(%r{spec/})
21
21
  spec.require_paths = ['lib']
22
22
 
23
- spec.add_dependency 'aptible-api', '~> 0.9.16'
23
+ spec.add_dependency 'aptible-api', '~> 0.9.20'
24
24
  spec.add_dependency 'aptible-auth', '~> 0.11.12'
25
25
  spec.add_dependency 'aptible-resource', '~> 0.3.6'
26
26
  spec.add_dependency 'thor', '~> 0.19.1'
@@ -29,10 +29,10 @@ Gem::Specification.new do |spec|
29
29
  spec.add_dependency 'chronic_duration', '~> 0.10.6'
30
30
  spec.add_dependency 'win32-process' if Gem.win_platform?
31
31
  spec.add_development_dependency 'bundler', '~> 1.3'
32
- spec.add_development_dependency 'aptible-tasks', '>= 0.2.0'
32
+ spec.add_development_dependency 'aptible-tasks', '~> 0.5.8'
33
33
  spec.add_development_dependency 'rake'
34
34
  spec.add_development_dependency 'rspec', '~> 3.2'
35
35
  spec.add_development_dependency 'pry'
36
- spec.add_development_dependency 'climate_control'
36
+ spec.add_development_dependency 'climate_control', '= 0.0.3'
37
37
  spec.add_development_dependency 'fabrication', '~> 2.15.2'
38
38
  end
@@ -55,14 +55,14 @@ module Aptible
55
55
 
56
56
  # Creates a local tunnel and yields the helper
57
57
 
58
- def with_local_tunnel(database, port = 0)
59
- op = database.create_operation!(type: 'tunnel', status: 'succeeded')
58
+ def with_local_tunnel(credential, port = 0)
59
+ op = credential.create_operation!(type: 'tunnel', status: 'succeeded')
60
60
 
61
- with_ssh_cmd(op) do |base_ssh_cmd, credential|
61
+ with_ssh_cmd(op) do |base_ssh_cmd, ssh_credential|
62
62
  ssh_cmd = base_ssh_cmd + ['-o', 'SendEnv=ACCESS_TOKEN']
63
63
  ssh_env = { 'ACCESS_TOKEN' => fetch_token }
64
64
 
65
- socket_path = credential.ssh_port_forward_socket
65
+ socket_path = ssh_credential.ssh_port_forward_socket
66
66
  tunnel_helper = Helpers::Tunnel.new(ssh_env, ssh_cmd, socket_path)
67
67
 
68
68
  tunnel_helper.start(port)
@@ -78,20 +78,48 @@ module Aptible
78
78
  raise Thor::Error, 'This command only works for PostgreSQL'
79
79
  end
80
80
 
81
- with_local_tunnel(database) do |tunnel_helper|
82
- auth = "aptible:#{database.passphrase}"
83
- host = "localhost.aptible.in:#{tunnel_helper.port}"
84
- yield "postgresql://#{auth}@#{host}/db"
81
+ credential = find_tunnel_credential(database)
82
+
83
+ with_local_tunnel(credential) do |tunnel_helper|
84
+ yield local_url(credential, tunnel_helper.port)
85
85
  end
86
86
  end
87
87
 
88
- def local_url(database, local_port)
89
- remote_url = database.connection_url
88
+ def local_url(credential, local_port)
89
+ remote_url = credential.connection_url
90
90
  uri = URI.parse(remote_url)
91
91
 
92
92
  "#{uri.scheme}://#{uri.user}:#{uri.password}@" \
93
93
  "localhost.aptible.in:#{local_port}#{uri.path}"
94
94
  end
95
+
96
+ def find_tunnel_credential(database, type = nil)
97
+ unless database.provisioned?
98
+ raise Thor::Error, "Database #{database.handle} is not provisioned"
99
+ end
100
+
101
+ finder = proc { |c| c.default }
102
+ finder = proc { |c| c.type == type } if type
103
+ credential = database.database_credentials.find(&finder)
104
+
105
+ return credential if credential
106
+
107
+ types = database.database_credentials.map(&:type)
108
+
109
+ # On v1, we fallback to the DB. We make sure to make --type work, to
110
+ # avoid a confusing experience for customers.
111
+ if database.account.stack.version == 'v1'
112
+ types << database.type
113
+ types.uniq!
114
+ return database if type.nil? || type == database.type
115
+ end
116
+
117
+ valid = types.join(', ')
118
+
119
+ err = 'No default credential for database'
120
+ err = "No credential with type #{type} for database" if type
121
+ raise Thor::Error, "#{err}, valid credential types: #{valid}"
122
+ end
95
123
  end
96
124
  end
97
125
  end
@@ -3,10 +3,30 @@ module Aptible
3
3
  module Helpers
4
4
  module Ssh
5
5
  def connect_to_ssh_portal(operation, *extra_ssh_args)
6
- # TODO: Should we rescue Interrupt here?
7
6
  with_ssh_cmd(operation) do |base_ssh_cmd|
8
7
  ssh_cmd = base_ssh_cmd + extra_ssh_args
9
- Kernel.system(*ssh_cmd)
8
+ begin
9
+ Kernel.system(*ssh_cmd)
10
+ rescue Interrupt
11
+ # Assuming we have a TTY, there are two cases here. Either SSH
12
+ # itself has a TTY, in which case it is controlling the TTY and
13
+ # the CLI won't be receiving SIGINT when CTRL+C is pressed, or
14
+ # SSH has no TTY, in which case the CLI and SSH are sharing the
15
+ # same process group, and will both receive SIGINT when CTRL+C
16
+ # is pressed and exit accordingly.
17
+ #
18
+ # I'm not sure how this *should* work on Windows, but it appears
19
+ # to work pretty much the same way, except that we'll get an ugly
20
+ # "Terminate batch job (Y/N)?" prompt in the no-TTY-for-SSH case,
21
+ # which we're likely to have a hard time handling.
22
+ #
23
+ # Note that this DOES NOT handle the case where the CLI is sent
24
+ # SIGINT by another process (as opposed to the line discipline).
25
+ # In this case, SSH will continue running in the background. This
26
+ # is something we should fix, but for now this 'simple' fix is
27
+ # enough to addresses the ugly stack trace we show when
28
+ # CTRL+C'ing out of logs.
29
+ end
10
30
  end
11
31
  end
12
32
 
@@ -71,16 +71,30 @@ module Aptible
71
71
  desc 'db:tunnel HANDLE', 'Create a local tunnel to a database'
72
72
  option :environment
73
73
  option :port, type: :numeric
74
+ option :type, type: :string
74
75
  define_method 'db:tunnel' do |handle|
75
76
  desired_port = Integer(options[:port] || 0)
76
77
  database = ensure_database(options.merge(db: handle))
77
- say 'Creating tunnel...', :green
78
78
 
79
- with_local_tunnel(database, desired_port) do |tunnel_helper|
79
+ credential = find_tunnel_credential(database, options[:type])
80
+
81
+ say "Creating #{credential.type} tunnel to #{database.handle}...",
82
+ :green
83
+
84
+ if options[:type].nil?
85
+ types = database.database_credentials.map(&:type)
86
+ unless types.empty?
87
+ valid = types.join(', ')
88
+ say 'Use --type TYPE to specify a tunnel type', :green
89
+ say "Valid types for #{database.handle}: #{valid}", :green
90
+ end
91
+ end
92
+
93
+ with_local_tunnel(credential, desired_port) do |tunnel_helper|
80
94
  port = tunnel_helper.port
81
- say "Connect at #{local_url(database, port)}", :green
95
+ say "Connect at #{local_url(credential, port)}", :green
82
96
 
83
- uri = URI(local_url(database, port))
97
+ uri = URI(local_url(credential, port))
84
98
  db = uri.path.gsub(%r{^/}, '')
85
99
  say 'Or, use the following arguments:', :green
86
100
  say("* Host: #{uri.host}", :green)
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.8.3'.freeze
3
+ VERSION = '0.8.4'.freeze
4
4
  end
5
5
  end
@@ -20,18 +20,114 @@ describe Aptible::CLI::Agent do
20
20
  end.to raise_error("Could not find database #{handle}")
21
21
  end
22
22
 
23
- it 'should print a message about how to connect' do
24
- allow(Aptible::Api::Database).to receive(:all) { [database] }
25
- local_url = 'postgresql://aptible:password@localhost.aptible.in:4242/db'
26
-
27
- expect(subject).to receive(:with_local_tunnel).with(database, 0)
28
- .and_yield(socat_helper)
29
- expect(subject).to receive(:say).with('Creating tunnel...', :green)
30
- expect(subject).to receive(:say).with("Connect at #{local_url}", :green)
31
-
32
- # db:tunnel should also explain each component of the URL:
33
- expect(subject).to receive(:say).exactly(7).times
34
- subject.send('db:tunnel', handle)
23
+ context 'valid database' do
24
+ before { allow(Aptible::Api::Database).to receive(:all) { [database] } }
25
+
26
+ it 'prints a message explaining how to connect' do
27
+ cred = Fabricate(:database_credential, default: true, type: 'foo',
28
+ database: database)
29
+
30
+ expect(subject).to receive(:with_local_tunnel).with(cred, 0)
31
+ .and_yield(socat_helper)
32
+
33
+ expect(subject).to receive(:say)
34
+ .with('Creating foo tunnel to foobar...', :green)
35
+
36
+ local_url = 'postgresql://aptible:password@localhost.aptible.in:4242/db'
37
+ expect(subject).to receive(:say)
38
+ .with("Connect at #{local_url}", :green)
39
+
40
+ # db:tunnel should also explain each component of the URL and suggest
41
+ # the --type argument:
42
+ expect(subject).to receive(:say).exactly(9).times
43
+ subject.send('db:tunnel', handle)
44
+ end
45
+
46
+ it 'defaults to a default credential' do
47
+ ok = Fabricate(:database_credential, default: true, database: database)
48
+ Fabricate(:database_credential, database: database, type: 'foo')
49
+ Fabricate(:database_credential, database: database, type: 'bar')
50
+
51
+ messages = []
52
+ allow(subject).to receive(:say) { |m, *| messages << m }
53
+ expect(subject).to receive(:with_local_tunnel).with(ok, 0)
54
+
55
+ subject.send('db:tunnel', handle)
56
+
57
+ expect(messages.grep(/use --type type/im)).not_to be_empty
58
+ expect(messages.grep(/valid types.*foo.*bar/im)).not_to be_empty
59
+ end
60
+
61
+ it 'supports --type' do
62
+ subject.options = { type: 'foo' }
63
+
64
+ Fabricate(:database_credential, default: true, database: database)
65
+ ok = Fabricate(:database_credential, type: 'foo', database: database)
66
+ Fabricate(:database_credential, type: 'bar', database: database)
67
+
68
+ allow(subject).to receive(:say)
69
+ expect(subject).to receive(:with_local_tunnel).with(ok, 0)
70
+ subject.send('db:tunnel', handle)
71
+ end
72
+
73
+ it 'fails when there is no default database credential nor type' do
74
+ Fabricate(:database_credential, default: false, database: database)
75
+
76
+ expect { subject.send('db:tunnel', handle) }
77
+ .to raise_error(/no default credential/im)
78
+ end
79
+
80
+ it 'fails when the type is incorrect' do
81
+ subject.options = { type: 'bar' }
82
+
83
+ Fabricate(:database_credential, type: 'foo', database: database)
84
+
85
+ expect { subject.send('db:tunnel', handle) }
86
+ .to raise_error(/no credential with type bar/im)
87
+ end
88
+
89
+ it 'fails when the database is not provisioned' do
90
+ database.stub(status: 'pending')
91
+
92
+ expect { subject.send('db:tunnel', handle) }
93
+ .to raise_error(/foobar is not provisioned/im)
94
+ end
95
+
96
+ context 'v1 stack' do
97
+ before { database.account.stack.stub(version: 'v1') }
98
+ before { allow(subject).to receive(:say) }
99
+
100
+ it 'falls back to the database itself if no type is given' do
101
+ expect(subject).to receive(:with_local_tunnel).with(database, 0)
102
+ subject.send('db:tunnel', handle)
103
+ end
104
+
105
+ it 'falls back to the database itself if type matches' do
106
+ subject.options = { type: 'bar' }
107
+ database.stub(type: 'bar')
108
+
109
+ expect(subject).to receive(:with_local_tunnel).with(database, 0)
110
+ subject.send('db:tunnel', handle)
111
+ end
112
+
113
+ it 'does not fall back to the database itself if type mismatches' do
114
+ subject.options = { type: 'bar' }
115
+ database.stub(type: 'foo')
116
+
117
+ expect { subject.send('db:tunnel', handle) }
118
+ .to raise_error(/no credential with type bar/im)
119
+ end
120
+
121
+ it 'does not suggest other types that do not exist' do
122
+ messages = []
123
+ allow(subject).to receive(:say) { |m, *| messages << m }
124
+ expect(subject).to receive(:with_local_tunnel).with(database, 0)
125
+
126
+ subject.send('db:tunnel', handle)
127
+
128
+ expect(messages.grep(/use --type type/im)).to be_empty
129
+ end
130
+ end
35
131
  end
36
132
  end
37
133
 
@@ -4,6 +4,7 @@ Fabricator(:account, from: :stub_account) do
4
4
  bastion_host 'localhost'
5
5
  dumptruck_port 1234
6
6
  handle 'aptible'
7
+ stack
7
8
 
8
9
  apps { [] }
9
10
  databases { [] }
@@ -0,0 +1,11 @@
1
+ class StubDatabaseCredential < OpenStruct; end
2
+
3
+ Fabricator(:database_credential, from: :stub_database_credential) do
4
+ database
5
+
6
+ default false
7
+ type 'postgresql'
8
+ connection_url 'postgresql://aptible:password@10.252.1.125:49158/db'
9
+
10
+ after_create { |credential| database.database_credentials << credential }
11
+ end
@@ -1,4 +1,8 @@
1
- class StubDatabase < OpenStruct; end
1
+ class StubDatabase < OpenStruct
2
+ def provisioned?
3
+ status == 'provisioned'
4
+ end
5
+ end
2
6
 
3
7
  Fabricator(:database, from: :stub_database) do
4
8
  type 'postgresql'
@@ -6,10 +10,12 @@ Fabricator(:database, from: :stub_database) do
6
10
  Fabricate.sequence(:database) { |i| "#{attrs[:type]}-#{i}" }
7
11
  end
8
12
  passphrase 'password'
13
+ status 'provisioned'
9
14
  connection_url 'postgresql://aptible:password@10.252.1.125:49158/db'
10
15
  account
11
16
 
12
17
  backups { [] }
18
+ database_credentials { [] }
13
19
 
14
20
  after_create { |database| database.account.databases << database }
15
21
  end
@@ -0,0 +1,9 @@
1
+ class StubStack < OpenStruct; end
2
+
3
+ Fabricator(:stack, from: :stub_stack) do
4
+ name 'foo'
5
+ version 'v2'
6
+
7
+ apps { [] }
8
+ databases { [] }
9
+ 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.8.3
4
+ version: 0.8.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Macreery
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-01-06 00:00:00.000000000 Z
11
+ date: 2017-01-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aptible-api
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.9.16
19
+ version: 0.9.20
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.9.16
26
+ version: 0.9.20
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: aptible-auth
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -126,16 +126,16 @@ dependencies:
126
126
  name: aptible-tasks
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - ">="
129
+ - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: 0.2.0
131
+ version: 0.5.8
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - ">="
136
+ - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: 0.2.0
138
+ version: 0.5.8
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: rake
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -182,16 +182,16 @@ dependencies:
182
182
  name: climate_control
183
183
  requirement: !ruby/object:Gem::Requirement
184
184
  requirements:
185
- - - ">="
185
+ - - '='
186
186
  - !ruby/object:Gem::Version
187
- version: '0'
187
+ version: 0.0.3
188
188
  type: :development
189
189
  prerelease: false
190
190
  version_requirements: !ruby/object:Gem::Requirement
191
191
  requirements:
192
- - - ">="
192
+ - - '='
193
193
  - !ruby/object:Gem::Version
194
- version: '0'
194
+ version: 0.0.3
195
195
  - !ruby/object:Gem::Dependency
196
196
  name: fabrication
197
197
  requirement: !ruby/object:Gem::Requirement
@@ -263,9 +263,11 @@ files:
263
263
  - spec/fabricators/account_fabricator.rb
264
264
  - spec/fabricators/app_fabricator.rb
265
265
  - spec/fabricators/backup_fabricator.rb
266
+ - spec/fabricators/database_credential_fabricator.rb
266
267
  - spec/fabricators/database_fabricator.rb
267
268
  - spec/fabricators/operation_fabricator.rb
268
269
  - spec/fabricators/service_fabricator.rb
270
+ - spec/fabricators/stack_fabricator.rb
269
271
  - spec/fabricators/vhost_fabricator.rb
270
272
  - spec/mock/git
271
273
  - spec/mock/ssh
@@ -315,9 +317,11 @@ test_files:
315
317
  - spec/fabricators/account_fabricator.rb
316
318
  - spec/fabricators/app_fabricator.rb
317
319
  - spec/fabricators/backup_fabricator.rb
320
+ - spec/fabricators/database_credential_fabricator.rb
318
321
  - spec/fabricators/database_fabricator.rb
319
322
  - spec/fabricators/operation_fabricator.rb
320
323
  - spec/fabricators/service_fabricator.rb
324
+ - spec/fabricators/stack_fabricator.rb
321
325
  - spec/fabricators/vhost_fabricator.rb
322
326
  - spec/mock/git
323
327
  - spec/mock/ssh