aptible-cli 0.8.3 → 0.8.4

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.
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