aptible-cli 0.16.2 → 0.16.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/lib/aptible/cli/agent.rb +62 -9
- data/lib/aptible/cli/helpers/database.rb +15 -0
- data/lib/aptible/cli/helpers/security_key.rb +136 -0
- data/lib/aptible/cli/helpers/system.rb +26 -0
- data/lib/aptible/cli/subcommands/db.rb +13 -0
- data/lib/aptible/cli/version.rb +1 -1
- data/spec/aptible/cli/agent_spec.rb +101 -9
- data/spec/aptible/cli/subcommands/db_spec.rb +56 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3eb92fffdc685a61fd06c0d5b38b485395eccb0e1762d8efeab0514e74c61b6b
|
4
|
+
data.tar.gz: 2237aacb034a1f7f7b1ae41328f0c05ee9393b7d48d6bdca93ca72072e867183
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3ad2d2c88fa090eb8f65a9f4b91b1c3bccf180764a69ef6d60c3f068e37b6fafbbea515f9a6451b2983aa6e44dcd82bee1020003069e644c3389a2011207764
|
7
|
+
data.tar.gz: 63cc2f56d9f14f9c5981cae0df45d1de16bf03ee714ddb8635641176ad8f6315d1a00ca154b8ad377274a7494d8ca268e9873ea0b51eb1aa8186a1094f444ad6
|
data/README.md
CHANGED
@@ -48,6 +48,7 @@ Commands:
|
|
48
48
|
aptible db:execute HANDLE SQL_FILE [--on-error-stop] # Executes sql against a database
|
49
49
|
aptible db:list # List all databases
|
50
50
|
aptible db:reload HANDLE # Reload a database
|
51
|
+
aptible db:replicate HANDLE REPLICA_HANDLE [--container-size SIZE_MB] [--size SIZE_GB] # Create a replica/follower of a database
|
51
52
|
aptible db:restart HANDLE [--container-size SIZE_MB] [--size SIZE_GB] # Restart a database
|
52
53
|
aptible db:tunnel HANDLE # Create a local tunnel to a database
|
53
54
|
aptible db:url HANDLE # Display a database URL
|
data/lib/aptible/cli/agent.rb
CHANGED
@@ -15,6 +15,8 @@ require_relative 'helpers/app_or_database'
|
|
15
15
|
require_relative 'helpers/vhost'
|
16
16
|
require_relative 'helpers/vhost/option_set_builder'
|
17
17
|
require_relative 'helpers/tunnel'
|
18
|
+
require_relative 'helpers/system'
|
19
|
+
require_relative 'helpers/security_key'
|
18
20
|
|
19
21
|
require_relative 'subcommands/apps'
|
20
22
|
require_relative 'subcommands/config'
|
@@ -39,6 +41,7 @@ module Aptible
|
|
39
41
|
|
40
42
|
include Helpers::Token
|
41
43
|
include Helpers::Ssh
|
44
|
+
include Helpers::System
|
42
45
|
include Subcommands::Apps
|
43
46
|
include Subcommands::Config
|
44
47
|
include Subcommands::DB
|
@@ -83,8 +86,9 @@ module Aptible
|
|
83
86
|
option :otp_token, desc: 'A token generated by your second-factor app'
|
84
87
|
def login
|
85
88
|
email = options[:email] || ask('Email: ')
|
86
|
-
password = options[:password] ||
|
87
|
-
|
89
|
+
password = options[:password] || ask_then_line(
|
90
|
+
'Password: ', echo: false
|
91
|
+
)
|
88
92
|
|
89
93
|
token_options = { email: email, password: password }
|
90
94
|
|
@@ -93,7 +97,7 @@ module Aptible
|
|
93
97
|
|
94
98
|
begin
|
95
99
|
lifetime = '1w'
|
96
|
-
lifetime = '12h' if token_options[:otp_token]
|
100
|
+
lifetime = '12h' if token_options[:otp_token] || token_options[:u2f]
|
97
101
|
lifetime = options[:lifetime] if options[:lifetime]
|
98
102
|
|
99
103
|
duration = ChronicDuration.parse(lifetime)
|
@@ -104,14 +108,63 @@ module Aptible
|
|
104
108
|
token_options[:expires_in] = duration
|
105
109
|
token = Aptible::Auth::Token.create(token_options)
|
106
110
|
rescue OAuth2::Error => e
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
+
# If a MFA is require but a token wasn't provided,
|
112
|
+
# prompt the user for MFA authentication and retry
|
113
|
+
if e.code != 'otp_token_required'
|
114
|
+
raise Thor::Error, 'Could not authenticate with given ' \
|
115
|
+
"credentials: #{e.code}"
|
111
116
|
end
|
112
117
|
|
113
|
-
|
114
|
-
|
118
|
+
u2f = (e.response.parsed['exception_context'] || {})['u2f']
|
119
|
+
|
120
|
+
q = Queue.new
|
121
|
+
mfa_threads = []
|
122
|
+
|
123
|
+
# If the user has added a security key and their computer supports it,
|
124
|
+
# allow them to use it
|
125
|
+
if u2f && !which('u2f-host').nil?
|
126
|
+
origin = Aptible::Auth::Resource.new.get.href
|
127
|
+
app_id = Aptible::Auth::Resource.new.utf_trusted_facets.href
|
128
|
+
|
129
|
+
challenge = u2f.fetch('challenge')
|
130
|
+
|
131
|
+
devices = u2f.fetch('devices').map do |dev|
|
132
|
+
Helpers::SecurityKey::Device.new(
|
133
|
+
dev.fetch('version'), dev.fetch('key_handle')
|
134
|
+
)
|
135
|
+
end
|
136
|
+
|
137
|
+
puts 'Enter your 2FA token or touch your Security Key once it ' \
|
138
|
+
'starts blinking.'
|
139
|
+
|
140
|
+
mfa_threads << Thread.new do
|
141
|
+
token_options[:u2f] = Helpers::SecurityKey.authenticate(
|
142
|
+
origin, app_id, challenge, devices
|
143
|
+
)
|
144
|
+
|
145
|
+
puts ''
|
146
|
+
|
147
|
+
q.push(nil)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
mfa_threads << Thread.new do
|
152
|
+
token_options[:otp_token] = options[:otp_token] || ask(
|
153
|
+
'2FA Token: '
|
154
|
+
)
|
155
|
+
|
156
|
+
q.push(nil)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Block until one of the threads completes
|
160
|
+
q.pop
|
161
|
+
|
162
|
+
mfa_threads.each do |thr|
|
163
|
+
sleep 0.5 until thr.status != 'run'
|
164
|
+
thr.kill
|
165
|
+
end.each(&:join)
|
166
|
+
|
167
|
+
retry
|
115
168
|
end
|
116
169
|
|
117
170
|
save_token(token.access_token)
|
@@ -47,6 +47,21 @@ module Aptible
|
|
47
47
|
databases_from_handle(dest_handle, source.account).first
|
48
48
|
end
|
49
49
|
|
50
|
+
def replicate_database(source, dest_handle, options)
|
51
|
+
replication_params = {
|
52
|
+
type: 'replicate',
|
53
|
+
handle: dest_handle,
|
54
|
+
container_size: options[:container_size],
|
55
|
+
disk_size: options[:size]
|
56
|
+
}.reject { |_, v| v.nil? }
|
57
|
+
op = source.create_operation!(replication_params)
|
58
|
+
attach_to_operation_logs(op)
|
59
|
+
|
60
|
+
replica = databases_from_handle(dest_handle, source.account).first
|
61
|
+
attach_to_operation_logs(replica.operations.last)
|
62
|
+
replica
|
63
|
+
end
|
64
|
+
|
50
65
|
# Creates a local tunnel and yields the helper
|
51
66
|
|
52
67
|
def with_local_tunnel(credential, port = 0)
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Aptible
|
2
|
+
module CLI
|
3
|
+
module Helpers
|
4
|
+
module SecurityKey
|
5
|
+
U2F_LOGGER = Logger.new(
|
6
|
+
ENV['U2F_DEBUG'] ? STDERR : File.open(File::NULL, 'w')
|
7
|
+
)
|
8
|
+
|
9
|
+
class AuthenticatorParameters
|
10
|
+
attr_reader :origin, :challenge, :app_id, :version, :key_handle
|
11
|
+
attr_reader :request
|
12
|
+
|
13
|
+
def initialize(origin, challenge, app_id, device)
|
14
|
+
@origin = origin
|
15
|
+
@challenge = challenge
|
16
|
+
@app_id = app_id
|
17
|
+
@version = device.version
|
18
|
+
@key_handle = device.key_handle
|
19
|
+
|
20
|
+
@request = {
|
21
|
+
'challenge' => challenge,
|
22
|
+
'appId' => app_id,
|
23
|
+
'version' => version,
|
24
|
+
'keyHandle' => key_handle
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class ThrottledAuthenticator
|
30
|
+
attr_reader :pid
|
31
|
+
|
32
|
+
def initialize(auth, pid)
|
33
|
+
@auth = auth
|
34
|
+
@pid = pid
|
35
|
+
end
|
36
|
+
|
37
|
+
def exited(_status)
|
38
|
+
[Authenticator.spawn(@auth), nil]
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.spawn(auth)
|
42
|
+
pid = Process.spawn(
|
43
|
+
'sleep', '2',
|
44
|
+
in: :close, out: :close, err: :close,
|
45
|
+
close_others: true
|
46
|
+
)
|
47
|
+
|
48
|
+
U2F_LOGGER.debug("#{self} #{auth.key_handle}: spawned #{pid}")
|
49
|
+
|
50
|
+
new(auth, pid)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Authenticator
|
55
|
+
attr_reader :pid
|
56
|
+
|
57
|
+
def initialize(auth, pid, out_read, err_read)
|
58
|
+
@auth = auth
|
59
|
+
@pid = pid
|
60
|
+
@out_read = out_read
|
61
|
+
@err_read = err_read
|
62
|
+
end
|
63
|
+
|
64
|
+
def exited(status)
|
65
|
+
out, err = [@out_read, @err_read].map(&:read).map(&:chomp)
|
66
|
+
|
67
|
+
if status.exitstatus == 0
|
68
|
+
U2F_LOGGER.info("#{self.class} #{@auth.key_handle}: ok: #{out}")
|
69
|
+
[nil, JSON.parse(out)]
|
70
|
+
else
|
71
|
+
U2F_LOGGER.warn("#{self.class} #{@auth.key_handle}: err: #{err}")
|
72
|
+
[ThrottledAuthenticator.spawn(@auth), nil]
|
73
|
+
end
|
74
|
+
ensure
|
75
|
+
[@out_read, @err_read].each(&:close)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.spawn(auth)
|
79
|
+
in_read, in_write = IO.pipe
|
80
|
+
out_read, out_write = IO.pipe
|
81
|
+
err_read, err_write = IO.pipe
|
82
|
+
|
83
|
+
pid = Process.spawn(
|
84
|
+
'u2f-host', '-aauthenticate', '-o', auth.origin,
|
85
|
+
in: in_read, out: out_write, err: err_write,
|
86
|
+
close_others: true
|
87
|
+
)
|
88
|
+
|
89
|
+
U2F_LOGGER.debug("#{self} #{auth.key_handle}: spawned #{pid}")
|
90
|
+
|
91
|
+
[in_read, out_write, err_write].each(&:close)
|
92
|
+
|
93
|
+
in_write.write(auth.request.to_json)
|
94
|
+
in_write.close
|
95
|
+
|
96
|
+
new(auth, pid, out_read, err_read)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class Device
|
101
|
+
attr_reader :version, :key_handle
|
102
|
+
|
103
|
+
def initialize(version, key_handle)
|
104
|
+
@version = version
|
105
|
+
@key_handle = key_handle
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.authenticate(origin, app_id, challenge, devices)
|
110
|
+
procs = Hash[devices.map do |device|
|
111
|
+
params = AuthenticatorParameters.new(
|
112
|
+
origin, challenge, app_id, device
|
113
|
+
)
|
114
|
+
w = Authenticator.spawn(params)
|
115
|
+
[w.pid, w]
|
116
|
+
end]
|
117
|
+
|
118
|
+
begin
|
119
|
+
loop do
|
120
|
+
pid, status = Process.wait2
|
121
|
+
w = procs.delete(pid)
|
122
|
+
raise "waited unknown pid: #{pid}" if w.nil?
|
123
|
+
|
124
|
+
r, out = w.exited(status)
|
125
|
+
|
126
|
+
procs[r.pid] = r if r
|
127
|
+
return out if out
|
128
|
+
end
|
129
|
+
ensure
|
130
|
+
procs.values.map(&:pid).each { |p| Process.kill(:SIGTERM, p) }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Aptible
|
2
|
+
module CLI
|
3
|
+
module Helpers
|
4
|
+
module System
|
5
|
+
def which(cmd)
|
6
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
7
|
+
|
8
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
9
|
+
exts.each do |ext|
|
10
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
11
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def ask_then_line(*args)
|
19
|
+
ret = ask(*args)
|
20
|
+
puts ''
|
21
|
+
ret
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -114,6 +114,19 @@ module Aptible
|
|
114
114
|
render_database(database, database.account)
|
115
115
|
end
|
116
116
|
|
117
|
+
desc 'db:replicate HANDLE REPLICA_HANDLE ' \
|
118
|
+
'[--container-size SIZE_MB] [--size SIZE_GB]',
|
119
|
+
'Create a replica/follower of a database'
|
120
|
+
option :environment
|
121
|
+
option :container_size, type: :numeric
|
122
|
+
option :size, type: :numeric
|
123
|
+
define_method 'db:replicate' do |source_handle, dest_handle|
|
124
|
+
source = ensure_database(options.merge(db: source_handle))
|
125
|
+
CLI.logger.info "Replicating #{source_handle}..."
|
126
|
+
database = replicate_database(source, dest_handle, options)
|
127
|
+
render_database(database.reload, database.account)
|
128
|
+
end
|
129
|
+
|
117
130
|
desc 'db:dump HANDLE [pg_dump options]',
|
118
131
|
'Dump a remote database to file'
|
119
132
|
option :environment
|
data/lib/aptible/cli/version.rb
CHANGED
@@ -30,10 +30,15 @@ describe Aptible::CLI::Agent do
|
|
30
30
|
let(:created_at) { Time.now }
|
31
31
|
let(:expires_at) { created_at + 1.week }
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
def make_oauth2_error(code, ctx = nil)
|
34
|
+
parsed = { 'error' => code }
|
35
|
+
parsed['exception_context'] = ctx if ctx
|
36
|
+
response = double('response', parsed: parsed, body: "error #{code}")
|
37
|
+
allow(response).to receive(:error=)
|
38
|
+
OAuth2::Error.new(response)
|
39
|
+
end
|
36
40
|
|
41
|
+
before do
|
37
42
|
allow(token).to receive(:access_token).and_return 'access_token'
|
38
43
|
allow(token).to receive(:created_at).and_return created_at
|
39
44
|
allow(token).to receive(:expires_at).and_return expires_at
|
@@ -55,7 +60,7 @@ describe Aptible::CLI::Agent do
|
|
55
60
|
|
56
61
|
it 'should raise an error if authentication fails' do
|
57
62
|
allow(Aptible::Auth::Token).to receive(:create)
|
58
|
-
.and_raise(
|
63
|
+
.and_raise(make_oauth2_error('foo'))
|
59
64
|
expect do
|
60
65
|
subject.login
|
61
66
|
end.to raise_error 'Could not authenticate with given credentials: foo'
|
@@ -87,7 +92,7 @@ describe Aptible::CLI::Agent do
|
|
87
92
|
expect { subject.login }.to raise_error(/Invalid token lifetime/)
|
88
93
|
end
|
89
94
|
|
90
|
-
context 'with
|
95
|
+
context 'with 2FA' do
|
91
96
|
let(:email) { 'foo@example.org' }
|
92
97
|
let(:password) { 'bar' }
|
93
98
|
let(:token) { '123456' }
|
@@ -124,7 +129,7 @@ describe Aptible::CLI::Agent do
|
|
124
129
|
expect(Aptible::Auth::Token).to receive(:create)
|
125
130
|
.with(email: email, password: password, expires_in: 1.week.seconds)
|
126
131
|
.once
|
127
|
-
.and_raise(
|
132
|
+
.and_raise(make_oauth2_error('otp_token_required'))
|
128
133
|
|
129
134
|
expect(Aptible::Auth::Token).to receive(:create)
|
130
135
|
.with(email: email, password: password, otp_token: token,
|
@@ -139,7 +144,7 @@ describe Aptible::CLI::Agent do
|
|
139
144
|
expect(Aptible::Auth::Token).to receive(:create)
|
140
145
|
.with(email: email, password: password, expires_in: 1.day.seconds)
|
141
146
|
.once
|
142
|
-
.and_raise(
|
147
|
+
.and_raise(make_oauth2_error('otp_token_required'))
|
143
148
|
|
144
149
|
expect(Aptible::Auth::Token).to receive(:create)
|
145
150
|
.with(email: email, password: password, otp_token: token,
|
@@ -155,17 +160,104 @@ describe Aptible::CLI::Agent do
|
|
155
160
|
expect(Aptible::Auth::Token).to receive(:create)
|
156
161
|
.with(email: email, password: password, expires_in: 1.week.seconds)
|
157
162
|
.once
|
158
|
-
.and_raise(
|
163
|
+
.and_raise(make_oauth2_error('otp_token_required'))
|
159
164
|
|
160
165
|
expect(Aptible::Auth::Token).to receive(:create)
|
161
166
|
.with(email: email, password: password, otp_token: token,
|
162
167
|
expires_in: 12.hours.seconds)
|
163
168
|
.once
|
164
|
-
.and_raise(
|
169
|
+
.and_raise(make_oauth2_error('foo'))
|
165
170
|
|
166
171
|
expect { subject.login }.to raise_error(/Could not authenticate/)
|
167
172
|
end
|
168
173
|
end
|
174
|
+
|
175
|
+
context 'with U2F' do
|
176
|
+
before do
|
177
|
+
allow(subject).to receive(:options)
|
178
|
+
.and_return(email: email, password: password)
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'shouldn\'t use U2F if not supported' do
|
182
|
+
allow(subject).to receive(:which)
|
183
|
+
.and_return(nil)
|
184
|
+
|
185
|
+
e = make_oauth2_error(
|
186
|
+
'otp_token_required',
|
187
|
+
'u2f' => {
|
188
|
+
'challenge' => 'some 123',
|
189
|
+
'devices' => [
|
190
|
+
{ 'version' => 'U2F_V2', 'key_handle' => '123' },
|
191
|
+
{ 'version' => 'U2F_V2', 'key_handle' => '456' }
|
192
|
+
]
|
193
|
+
}
|
194
|
+
)
|
195
|
+
|
196
|
+
expect(Aptible::Auth::Token).to receive(:create)
|
197
|
+
.with(email: email, password: password, expires_in: 1.week.seconds)
|
198
|
+
.once
|
199
|
+
.and_raise(e)
|
200
|
+
|
201
|
+
expect(Aptible::CLI::Helpers::SecurityKey).not_to \
|
202
|
+
receive(:authenticate)
|
203
|
+
|
204
|
+
expect(subject).to receive(:ask).with('2FA Token: ')
|
205
|
+
.once
|
206
|
+
.and_return(token)
|
207
|
+
|
208
|
+
expect(Aptible::Auth::Token).to receive(:create)
|
209
|
+
.with(email: email, password: password, otp_token: token,
|
210
|
+
expires_in: 12.hours.seconds)
|
211
|
+
.once
|
212
|
+
.and_return(token)
|
213
|
+
|
214
|
+
subject.login
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'should call into U2F if supported' do
|
218
|
+
allow(subject).to receive(:which).and_return('u2f-host')
|
219
|
+
allow(subject).to receive(:ask).with('2FA Token: ') { sleep }
|
220
|
+
|
221
|
+
e = make_oauth2_error(
|
222
|
+
'otp_token_required',
|
223
|
+
'u2f' => {
|
224
|
+
'challenge' => 'some 123',
|
225
|
+
'devices' => [
|
226
|
+
{ 'version' => 'U2F_V2', 'key_handle' => '123' },
|
227
|
+
{ 'version' => 'U2F_V2', 'key_handle' => '456' }
|
228
|
+
]
|
229
|
+
}
|
230
|
+
)
|
231
|
+
|
232
|
+
u2f = double('u2f response')
|
233
|
+
|
234
|
+
expect(Aptible::Auth::Token).to receive(:create)
|
235
|
+
.with(email: email, password: password, expires_in: 1.week.seconds)
|
236
|
+
.once
|
237
|
+
.and_raise(e)
|
238
|
+
|
239
|
+
expect(subject).to receive(:puts).with(/security key/i)
|
240
|
+
|
241
|
+
expect(Aptible::CLI::Helpers::SecurityKey).to receive(:authenticate)
|
242
|
+
.with(
|
243
|
+
'https://auth.aptible.com/',
|
244
|
+
'https://auth.aptible.com/u2f/trusted_facets',
|
245
|
+
'some 123',
|
246
|
+
array_including(
|
247
|
+
instance_of(Aptible::CLI::Helpers::SecurityKey::Device),
|
248
|
+
instance_of(Aptible::CLI::Helpers::SecurityKey::Device)
|
249
|
+
)
|
250
|
+
).and_return(u2f)
|
251
|
+
|
252
|
+
expect(Aptible::Auth::Token).to receive(:create)
|
253
|
+
.with(email: email, password: password, u2f: u2f,
|
254
|
+
expires_in: 12.hours.seconds)
|
255
|
+
.once
|
256
|
+
.and_return(token)
|
257
|
+
|
258
|
+
subject.login
|
259
|
+
end
|
260
|
+
end
|
169
261
|
end
|
170
262
|
end
|
171
263
|
|
@@ -439,6 +439,62 @@ describe Aptible::CLI::Agent do
|
|
439
439
|
end
|
440
440
|
end
|
441
441
|
|
442
|
+
describe '#db:replicate' do
|
443
|
+
let(:databases) { [] }
|
444
|
+
before { allow(Aptible::Api::Database).to receive(:all) { databases } }
|
445
|
+
|
446
|
+
def expect_replicate_database(opts = {})
|
447
|
+
master = Fabricate(:database, handle: 'master')
|
448
|
+
databases << master
|
449
|
+
replica = Fabricate(:database,
|
450
|
+
account: master.account,
|
451
|
+
handle: 'replica')
|
452
|
+
|
453
|
+
op = Fabricate(:operation)
|
454
|
+
|
455
|
+
params = { type: 'replicate', handle: 'replica' }.merge(opts)
|
456
|
+
params[:disk_size] = params.delete(:size) if params[:size]
|
457
|
+
expect(master).to receive(:create_operation!)
|
458
|
+
.with(**params).and_return(op)
|
459
|
+
|
460
|
+
expect(subject).to receive(:attach_to_operation_logs).with(op) do
|
461
|
+
databases << replica
|
462
|
+
replica
|
463
|
+
end
|
464
|
+
|
465
|
+
provision = Fabricate(:operation)
|
466
|
+
|
467
|
+
expect(replica).to receive_message_chain(:operations, :last)
|
468
|
+
.and_return(provision)
|
469
|
+
|
470
|
+
expect(subject).to receive(:attach_to_operation_logs).with(provision)
|
471
|
+
|
472
|
+
expect(replica).to receive(:reload).and_return(replica)
|
473
|
+
|
474
|
+
subject.options = opts
|
475
|
+
subject.send('db:replicate', 'master', 'replica')
|
476
|
+
|
477
|
+
expect(captured_logs).to match(/replicating master/i)
|
478
|
+
end
|
479
|
+
|
480
|
+
it 'allows replicating an existing database' do
|
481
|
+
expect_replicate_database
|
482
|
+
end
|
483
|
+
|
484
|
+
it 'allows replicating a database with a container size' do
|
485
|
+
expect_replicate_database(container_size: 40)
|
486
|
+
end
|
487
|
+
|
488
|
+
it 'allows replicating a database with a disk size' do
|
489
|
+
expect_replicate_database(size: 40)
|
490
|
+
end
|
491
|
+
|
492
|
+
it 'fails if the DB is not found' do
|
493
|
+
expect { subject.send('db:replicate', 'nope', 'replica') }
|
494
|
+
.to raise_error(Thor::Error, 'Could not find database nope')
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
442
498
|
describe '#db:dump' do
|
443
499
|
it 'should fail if database is non-existent' do
|
444
500
|
allow(Aptible::Api::Database).to receive(:all) { [] }
|
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.16.
|
4
|
+
version: 0.16.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Frank Macreery
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aptible-resource
|
@@ -277,7 +277,9 @@ files:
|
|
277
277
|
- lib/aptible/cli/helpers/database.rb
|
278
278
|
- lib/aptible/cli/helpers/environment.rb
|
279
279
|
- lib/aptible/cli/helpers/operation.rb
|
280
|
+
- lib/aptible/cli/helpers/security_key.rb
|
280
281
|
- lib/aptible/cli/helpers/ssh.rb
|
282
|
+
- lib/aptible/cli/helpers/system.rb
|
281
283
|
- lib/aptible/cli/helpers/token.rb
|
282
284
|
- lib/aptible/cli/helpers/tunnel.rb
|
283
285
|
- lib/aptible/cli/helpers/vhost.rb
|
@@ -373,7 +375,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
373
375
|
version: '0'
|
374
376
|
requirements: []
|
375
377
|
rubyforge_project:
|
376
|
-
rubygems_version: 2.7.6
|
378
|
+
rubygems_version: 2.7.6.2
|
377
379
|
signing_key:
|
378
380
|
specification_version: 4
|
379
381
|
summary: Command-line interface for Aptible services
|