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 +4 -4
- data/aptible-cli.gemspec +3 -3
- data/lib/aptible/cli/helpers/database.rb +38 -10
- data/lib/aptible/cli/helpers/ssh.rb +22 -2
- data/lib/aptible/cli/subcommands/db.rb +18 -4
- data/lib/aptible/cli/version.rb +1 -1
- data/spec/aptible/cli/subcommands/db_spec.rb +108 -12
- data/spec/fabricators/account_fabricator.rb +1 -0
- data/spec/fabricators/database_credential_fabricator.rb +11 -0
- data/spec/fabricators/database_fabricator.rb +7 -1
- data/spec/fabricators/stack_fabricator.rb +9 -0
- metadata +16 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cd702a7afbcc8290e375387617c228b388b69918
|
4
|
+
data.tar.gz: bbbcb95f4ebc9deac6671049c7534c83e2127d1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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', '
|
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(
|
59
|
-
op =
|
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,
|
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 =
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
yield
|
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(
|
89
|
-
remote_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
|
-
|
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
|
-
|
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(
|
95
|
+
say "Connect at #{local_url(credential, port)}", :green
|
82
96
|
|
83
|
-
uri = URI(local_url(
|
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)
|
data/lib/aptible/cli/version.rb
CHANGED
@@ -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
|
-
|
24
|
-
allow(Aptible::Api::Database).to receive(:all) { [database] }
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
|
@@ -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
|
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
|
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.
|
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-
|
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.
|
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.
|
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.
|
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.
|
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:
|
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:
|
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
|