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