aptible-cli 0.14.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -1
  3. data/aptible-cli.gemspec +1 -0
  4. data/bin/aptible +9 -5
  5. data/lib/aptible/cli.rb +36 -0
  6. data/lib/aptible/cli/agent.rb +10 -6
  7. data/lib/aptible/cli/error.rb +6 -0
  8. data/lib/aptible/cli/formatter.rb +21 -0
  9. data/lib/aptible/cli/formatter/grouped_keyed_list.rb +54 -0
  10. data/lib/aptible/cli/formatter/keyed_list.rb +25 -0
  11. data/lib/aptible/cli/formatter/keyed_object.rb +16 -0
  12. data/lib/aptible/cli/formatter/list.rb +33 -0
  13. data/lib/aptible/cli/formatter/node.rb +8 -0
  14. data/lib/aptible/cli/formatter/object.rb +38 -0
  15. data/lib/aptible/cli/formatter/root.rb +46 -0
  16. data/lib/aptible/cli/formatter/value.rb +25 -0
  17. data/lib/aptible/cli/helpers/app.rb +1 -0
  18. data/lib/aptible/cli/helpers/database.rb +22 -6
  19. data/lib/aptible/cli/helpers/operation.rb +3 -2
  20. data/lib/aptible/cli/helpers/tunnel.rb +1 -3
  21. data/lib/aptible/cli/helpers/vhost.rb +9 -46
  22. data/lib/aptible/cli/renderer.rb +26 -0
  23. data/lib/aptible/cli/renderer/base.rb +8 -0
  24. data/lib/aptible/cli/renderer/json.rb +26 -0
  25. data/lib/aptible/cli/renderer/text.rb +99 -0
  26. data/lib/aptible/cli/resource_formatter.rb +136 -0
  27. data/lib/aptible/cli/subcommands/apps.rb +26 -14
  28. data/lib/aptible/cli/subcommands/backup.rb +22 -4
  29. data/lib/aptible/cli/subcommands/config.rb +15 -11
  30. data/lib/aptible/cli/subcommands/db.rb +82 -31
  31. data/lib/aptible/cli/subcommands/deploy.rb +1 -1
  32. data/lib/aptible/cli/subcommands/endpoints.rb +11 -8
  33. data/lib/aptible/cli/subcommands/operation.rb +2 -1
  34. data/lib/aptible/cli/subcommands/rebuild.rb +1 -1
  35. data/lib/aptible/cli/subcommands/restart.rb +1 -1
  36. data/lib/aptible/cli/subcommands/services.rb +8 -9
  37. data/lib/aptible/cli/version.rb +1 -1
  38. data/spec/aptible/cli/agent_spec.rb +11 -14
  39. data/spec/aptible/cli/formatter_spec.rb +4 -0
  40. data/spec/aptible/cli/renderer/json_spec.rb +63 -0
  41. data/spec/aptible/cli/renderer/text_spec.rb +150 -0
  42. data/spec/aptible/cli/resource_formatter_spec.rb +113 -0
  43. data/spec/aptible/cli/subcommands/apps_spec.rb +144 -28
  44. data/spec/aptible/cli/subcommands/backup_spec.rb +37 -16
  45. data/spec/aptible/cli/subcommands/config_spec.rb +95 -0
  46. data/spec/aptible/cli/subcommands/db_spec.rb +185 -93
  47. data/spec/aptible/cli/subcommands/endpoints_spec.rb +10 -8
  48. data/spec/aptible/cli/subcommands/operation_spec.rb +0 -1
  49. data/spec/aptible/cli/subcommands/rebuild_spec.rb +17 -0
  50. data/spec/aptible/cli/subcommands/services_spec.rb +8 -12
  51. data/spec/aptible/cli_spec.rb +31 -0
  52. data/spec/fabricators/account_fabricator.rb +11 -0
  53. data/spec/fabricators/app_fabricator.rb +15 -0
  54. data/spec/fabricators/configuration_fabricator.rb +8 -0
  55. data/spec/fabricators/database_image_fabricator.rb +17 -0
  56. data/spec/fabricators/operation_fabricator.rb +1 -0
  57. data/spec/fabricators/service_fabricator.rb +4 -0
  58. data/spec/spec_helper.rb +63 -1
  59. metadata +55 -4
  60. data/spec/aptible/cli/helpers/vhost_spec.rb +0 -105
@@ -4,10 +4,12 @@ class SocatHelperMock < OpenStruct
4
4
  end
5
5
 
6
6
  describe Aptible::CLI::Agent do
7
+ let(:token) { double('token') }
8
+
7
9
  before do
8
10
  allow(subject).to receive(:ask)
9
11
  allow(subject).to receive(:save_token)
10
- allow(subject).to receive(:fetch_token) { double 'token' }
12
+ allow(subject).to receive(:fetch_token) { token }
11
13
  end
12
14
 
13
15
  let(:handle) { 'foobar' }
@@ -15,79 +17,114 @@ describe Aptible::CLI::Agent do
15
17
  let(:socat_helper) { SocatHelperMock.new(port: 4242) }
16
18
 
17
19
  describe '#db:create' do
18
- let(:db) { Fabricate(:database) }
19
- let(:op) { Fabricate(:operation) }
20
- let(:account) { Fabricate(:account) }
21
-
22
20
  before do
23
21
  allow(Aptible::Api::Account).to receive(:all).and_return([account])
24
- allow(db).to receive(:reload).and_return(db)
25
- allow(op).to receive(:errors).and_return(Aptible::Resource::Errors.new)
26
22
  end
27
23
 
28
- it 'creates a new DB' do
24
+ def expect_provision_database(create_opts, provision_opts = {})
25
+ db = Fabricate(:database)
26
+ expect(db).to receive(:reload).and_return(db)
27
+
28
+ op = Fabricate(:operation)
29
+
29
30
  expect(account).to receive(:create_database!)
30
- .with(handle: 'foo', type: 'postgresql')
31
- .and_return(db)
31
+ .with(**create_opts).and_return(db)
32
32
 
33
33
  expect(db).to receive(:create_operation)
34
- .with(type: 'provision')
35
- .and_return(op)
34
+ .with(type: 'provision', **provision_opts).and_return(op)
35
+
36
+ expect(subject).to receive(:attach_to_operation_logs).with(op)
37
+ end
38
+
39
+ let(:account) { Fabricate(:account) }
36
40
 
37
- expect(subject).to receive(:attach_to_operation_logs)
38
- .with(op)
41
+ it 'creates a new DB' do
42
+ expect_provision_database(handle: 'foo', type: 'postgresql')
39
43
 
40
44
  subject.options = { type: 'postgresql' }
41
45
  subject.send('db:create', 'foo')
42
46
  end
43
47
 
44
48
  it 'creates a new DB with a container size' do
45
- expect(account).to receive(:create_database!)
46
- .with(handle: 'foo', type: 'postgresql', initial_container_size: 1024)
47
- .and_return(db)
48
-
49
- expect(db).to receive(:create_operation)
50
- .with(type: 'provision', container_size: 1024)
51
- .and_return(op)
52
-
53
- expect(subject).to receive(:attach_to_operation_logs)
54
- .with(op)
49
+ expect_provision_database(
50
+ { handle: 'foo', type: 'postgresql', initial_container_size: 1024 },
51
+ { container_size: 1024 }
52
+ )
55
53
 
56
54
  subject.options = { type: 'postgresql', container_size: 1024 }
57
55
  subject.send('db:create', 'foo')
58
56
  end
59
57
 
60
58
  it 'creates a new DB with a disk size' do
61
- expect(account).to receive(:create_database!)
62
- .with(handle: 'foo', type: 'postgresql', initial_disk_size: 200)
63
- .and_return(db)
64
-
65
- expect(db).to receive(:create_operation)
66
- .with(type: 'provision', disk_size: 200)
67
- .and_return(op)
68
-
69
- expect(subject).to receive(:attach_to_operation_logs)
70
- .with(op)
59
+ expect_provision_database(
60
+ { handle: 'foo', type: 'postgresql', initial_disk_size: 200 },
61
+ { disk_size: 200 }
62
+ )
71
63
 
72
64
  subject.options = { type: 'postgresql', size: 200 }
73
65
  subject.send('db:create', 'foo')
74
66
  end
75
67
 
76
68
  it 'deprovisions the database if the operation cannot be created' do
77
- op.errors.full_messages << 'oops!'
69
+ db = Fabricate(:database)
70
+
71
+ provision_op = Fabricate(:operation)
72
+ provision_op.errors.full_messages << 'oops'
73
+
74
+ deprovision_op = Fabricate(:operation)
78
75
 
79
76
  expect(account).to receive(:create_database!).and_return(db)
80
77
 
81
78
  expect(db).to receive(:create_operation)
82
- .with(type: 'provision')
83
- .once.ordered.and_return(op)
79
+ .with(type: 'provision').once.ordered.and_return(provision_op)
84
80
 
85
81
  expect(db).to receive(:create_operation!)
86
- .with(type: 'deprovision')
87
- .once.ordered
82
+ .with(type: 'deprovision').once.ordered.and_return(deprovision_op)
88
83
 
89
84
  expect { subject.send('db:create', 'foo') }.to raise_error(/oops/im)
90
85
  end
86
+
87
+ context 'with version' do
88
+ let(:img) do
89
+ Fabricate(:database_image, type: 'postgresql', version: '9.4')
90
+ end
91
+
92
+ let(:alt_version) do
93
+ Fabricate(:database_image, type: 'postgresql', version: '10')
94
+ end
95
+
96
+ let(:alt_type) do
97
+ Fabricate(:database_image, type: 'redis', version: '9.4')
98
+ end
99
+
100
+ before do
101
+ allow(Aptible::Api::DatabaseImage).to receive(:all)
102
+ .with(token: token).and_return([alt_version, alt_type, img])
103
+ end
104
+
105
+ it 'provisions a Database with a matching Database Image' do
106
+ expect_provision_database(
107
+ handle: 'foo',
108
+ type: 'postgresql',
109
+ database_image: img
110
+ )
111
+
112
+ subject.options = { type: 'postgresql', version: '9.4' }
113
+ subject.send('db:create', 'foo')
114
+ end
115
+
116
+ it 'fails if the Database Image does not exist' do
117
+ subject.options = { type: 'postgresql', version: '123' }
118
+ expect { subject.send('db:create', 'foo') }
119
+ .to raise_error(Thor::Error, /no database image/i)
120
+ end
121
+
122
+ it 'fails if type is not passed' do
123
+ subject.options = { version: '123' }
124
+ expect { subject.send('db:create', 'foo') }
125
+ .to raise_error(Thor::Error, /type is required/i)
126
+ end
127
+ end
91
128
  end
92
129
 
93
130
  describe '#db:tunnel' do
@@ -108,17 +145,20 @@ describe Aptible::CLI::Agent do
108
145
  expect(subject).to receive(:with_local_tunnel).with(cred, 0)
109
146
  .and_yield(socat_helper)
110
147
 
111
- expect(subject).to receive(:say)
112
- .with('Creating foo tunnel to foobar...', :green)
148
+ subject.send('db:tunnel', handle)
113
149
 
114
150
  local_url = 'postgresql://aptible:password@localhost.aptible.in:4242/db'
115
- expect(subject).to receive(:say)
116
- .with("Connect at #{local_url}", :green)
117
151
 
118
- # db:tunnel should also explain each component of the URL and suggest
119
- # the --type argument:
120
- expect(subject).to receive(:say).exactly(9).times
121
- subject.send('db:tunnel', handle)
152
+ expect(captured_logs)
153
+ .to match(/creating foo tunnel to foobar/i)
154
+ expect(captured_logs)
155
+ .to match(/connect at #{Regexp.escape(local_url)}/i)
156
+
157
+ expect(captured_logs).to match(/host: localhost\.aptible\.in/i)
158
+ expect(captured_logs).to match(/port: 4242/i)
159
+ expect(captured_logs).to match(/username: aptible/i)
160
+ expect(captured_logs).to match(/password: password/i)
161
+ expect(captured_logs).to match(/database: db/i)
122
162
  end
123
163
 
124
164
  it 'defaults to a default credential' do
@@ -126,14 +166,12 @@ describe Aptible::CLI::Agent do
126
166
  Fabricate(:database_credential, database: database, type: 'foo')
127
167
  Fabricate(:database_credential, database: database, type: 'bar')
128
168
 
129
- messages = []
130
- allow(subject).to receive(:say) { |m, *| messages << m }
131
169
  expect(subject).to receive(:with_local_tunnel).with(ok, 0)
132
170
 
133
171
  subject.send('db:tunnel', handle)
134
172
 
135
- expect(messages.grep(/use --type type/im)).not_to be_empty
136
- expect(messages.grep(/valid types.*foo.*bar/im)).not_to be_empty
173
+ expect(captured_logs).to match(/use --type type/i)
174
+ expect(captured_logs).to match(/valid types.*foo.*bar/i)
137
175
  end
138
176
 
139
177
  it 'supports --type' do
@@ -143,7 +181,6 @@ describe Aptible::CLI::Agent do
143
181
  ok = Fabricate(:database_credential, type: 'foo', database: database)
144
182
  Fabricate(:database_credential, type: 'bar', database: database)
145
183
 
146
- allow(subject).to receive(:say)
147
184
  expect(subject).to receive(:with_local_tunnel).with(ok, 0)
148
185
  subject.send('db:tunnel', handle)
149
186
  end
@@ -174,7 +211,6 @@ describe Aptible::CLI::Agent do
174
211
  context 'v1 stack' do
175
212
  before do
176
213
  allow(database.account.stack).to receive(:version) { 'v1' }
177
- allow(subject).to receive(:say)
178
214
  end
179
215
 
180
216
  it 'falls back to the database itself if no type is given' do
@@ -199,13 +235,11 @@ describe Aptible::CLI::Agent do
199
235
  end
200
236
 
201
237
  it 'does not suggest other types that do not exist' do
202
- messages = []
203
- allow(subject).to receive(:say) { |m, *| messages << m }
204
238
  expect(subject).to receive(:with_local_tunnel).with(database, 0)
205
239
 
206
240
  subject.send('db:tunnel', handle)
207
241
 
208
- expect(messages.grep(/use --type type/im)).to be_empty
242
+ expect(captured_logs).not_to match(/use --type type/i)
209
243
  end
210
244
  end
211
245
  end
@@ -216,9 +250,14 @@ describe Aptible::CLI::Agent do
216
250
  staging = Fabricate(:account, handle: 'staging')
217
251
  prod = Fabricate(:account, handle: 'production')
218
252
 
219
- [[staging, 'staging-redis-db'], [staging, 'staging-postgres-db'],
220
- [prod, 'prod-elsearch-db'], [prod, 'prod-postgres-db']].each do |a, h|
221
- Fabricate(:database, account: a, handle: h)
253
+ [
254
+ [staging, 'staging-redis-db'],
255
+ [staging, 'staging-postgres-db'],
256
+ [prod, 'prod-elsearch-db'],
257
+ [prod, 'prod-postgres-db']
258
+ ].each do |a, h|
259
+ d = Fabricate(:database, account: a, handle: h)
260
+ Fabricate(:database_credential, database: d)
222
261
  end
223
262
 
224
263
  token = 'the-token'
@@ -229,45 +268,38 @@ describe Aptible::CLI::Agent do
229
268
 
230
269
  context 'when no account is specified' do
231
270
  it 'prints out the grouped database handles for all accounts' do
232
- allow(subject).to receive(:say)
233
-
234
271
  subject.send('db:list')
235
272
 
236
- expect(subject).to have_received(:say).with('=== staging')
237
- expect(subject).to have_received(:say).with('staging-redis-db')
238
- expect(subject).to have_received(:say).with('staging-postgres-db')
273
+ expect(captured_output_text).to include('=== staging')
274
+ expect(captured_output_text).to include('staging-redis-db')
275
+ expect(captured_output_text).to include('staging-postgres-db')
239
276
 
240
- expect(subject).to have_received(:say).with('=== production')
241
- expect(subject).to have_received(:say).with('prod-elsearch-db')
242
- expect(subject).to have_received(:say).with('prod-postgres-db')
277
+ expect(captured_output_text).to include('=== production')
278
+ expect(captured_output_text).to include('prod-elsearch-db')
279
+ expect(captured_output_text).to include('prod-postgres-db')
243
280
  end
244
281
  end
245
282
 
246
283
  context 'when a valid account is specified' do
247
284
  it 'prints out the database handles for the account' do
248
- allow(subject).to receive(:say)
249
-
250
285
  subject.options = { environment: 'staging' }
251
286
  subject.send('db:list')
252
287
 
253
- expect(subject).to have_received(:say).with('=== staging')
254
- expect(subject).to have_received(:say).with('staging-redis-db')
255
- expect(subject).to have_received(:say).with('staging-postgres-db')
288
+ expect(captured_output_text).to include('=== staging')
289
+ expect(captured_output_text).to include('staging-redis-db')
290
+ expect(captured_output_text).to include('staging-postgres-db')
256
291
 
257
- expect(subject).to_not have_received(:say).with('=== production')
258
- expect(subject).to_not have_received(:say).with('prod-elsearch-db')
259
- expect(subject).to_not have_received(:say).with('prod-postgres-db')
292
+ expect(captured_output_text).not_to include('=== production')
293
+ expect(captured_output_text).not_to include('prod-elsearch-db')
294
+ expect(captured_output_text).not_to include('prod-postgres-db')
260
295
  end
261
296
  end
262
297
 
263
298
  context 'when an invalid account is specified' do
264
299
  it 'prints out an error' do
265
- allow(subject).to receive(:say)
266
-
267
300
  subject.options = { environment: 'foo' }
268
- expect { subject.send('db:list') }.to raise_error(
269
- 'Specified account does not exist'
270
- )
301
+ expect { subject.send('db:list') }
302
+ .to raise_error('Specified account does not exist')
271
303
  end
272
304
  end
273
305
  end
@@ -281,10 +313,11 @@ describe Aptible::CLI::Agent do
281
313
  it 'allows creating a new backup' do
282
314
  expect(database).to receive(:create_operation!)
283
315
  .with(type: 'backup').and_return(op)
284
- expect(subject).to receive(:say).with('Backing up foobar...')
285
316
  expect(subject).to receive(:attach_to_operation_logs).with(op)
286
317
 
287
318
  subject.send('db:backup', handle)
319
+
320
+ expect(captured_logs).to match(/backing up foobar/i)
288
321
  end
289
322
 
290
323
  it 'fails if the DB is not found' do
@@ -302,10 +335,11 @@ describe Aptible::CLI::Agent do
302
335
  it 'allows reloading a database' do
303
336
  expect(database).to receive(:create_operation!)
304
337
  .with(type: 'reload').and_return(op)
305
- expect(subject).to receive(:say).with('Reloading foobar...')
306
338
  expect(subject).to receive(:attach_to_operation_logs).with(op)
307
339
 
308
340
  subject.send('db:reload', handle)
341
+
342
+ expect(captured_logs).to match(/reloading foobar/i)
309
343
  end
310
344
 
311
345
  it 'fails if the DB is not found' do
@@ -324,32 +358,35 @@ describe Aptible::CLI::Agent do
324
358
  expect(database).to receive(:create_operation!)
325
359
  .with(type: 'restart').and_return(op)
326
360
 
327
- expect(subject).to receive(:say).with('Restarting foobar...')
328
361
  expect(subject).to receive(:attach_to_operation_logs).with(op)
329
362
 
330
363
  subject.send('db:restart', handle)
364
+
365
+ expect(captured_logs).to match(/restarting foobar/i)
331
366
  end
332
367
 
333
368
  it 'allows restarting a database with a container size' do
334
369
  expect(database).to receive(:create_operation!)
335
370
  .with(type: 'restart', container_size: 40).and_return(op)
336
371
 
337
- expect(subject).to receive(:say).with('Restarting foobar...')
338
372
  expect(subject).to receive(:attach_to_operation_logs).with(op)
339
373
 
340
374
  subject.options = { container_size: 40 }
341
375
  subject.send('db:restart', handle)
376
+
377
+ expect(captured_logs).to match(/restarting foobar/i)
342
378
  end
343
379
 
344
380
  it 'allows restarting a database with a disk size' do
345
381
  expect(database).to receive(:create_operation!)
346
382
  .with(type: 'restart', disk_size: 40).and_return(op)
347
383
 
348
- expect(subject).to receive(:say).with('Restarting foobar...')
349
384
  expect(subject).to receive(:attach_to_operation_logs).with(op)
350
385
 
351
386
  subject.options = { size: 40 }
352
387
  subject.send('db:restart', handle)
388
+
389
+ expect(captured_logs).to match(/restarting foobar/i)
353
390
  end
354
391
 
355
392
  it 'fails if the DB is not found' do
@@ -369,13 +406,12 @@ describe Aptible::CLI::Agent do
369
406
 
370
407
  context 'valid database' do
371
408
  it 'returns the URL of a specified DB' do
372
- cred = Fabricate(:database_credential, default: true, type: 'foo',
373
- database: database)
374
-
375
- expect(subject).to receive(:say).with(cred.connection_url)
409
+ cred = Fabricate(
410
+ :database_credential, default: true, type: 'foo', database: database
411
+ )
376
412
  expect(database).not_to receive(:connection_url)
377
-
378
413
  subject.send('db:url', handle)
414
+ expect(captured_output_text.chomp).to eq(cred.connection_url)
379
415
  end
380
416
 
381
417
  it 'fails if multiple DBs are found' do
@@ -388,19 +424,75 @@ describe Aptible::CLI::Agent do
388
424
  context 'v1 stack' do
389
425
  before do
390
426
  allow(database.account.stack).to receive(:version) { 'v1' }
391
- allow(subject).to receive(:say)
392
427
  end
393
428
 
394
429
  it 'returns the URL of a specified DB' do
395
430
  connection_url = 'postgresql://aptible-v1:password@lega.cy:4242/db'
396
-
397
- expect(subject).to receive(:say).with(connection_url)
398
431
  expect(database).to receive(:connection_url)
399
432
  .and_return(connection_url)
400
433
 
401
434
  subject.send('db:url', handle)
435
+
436
+ expect(captured_output_text.chomp).to eq(connection_url)
402
437
  end
403
438
  end
404
439
  end
405
440
  end
441
+
442
+ describe '#db:deprovision' do
443
+ before { expect(Aptible::Api::Database).to receive(:all) { [database] } }
444
+
445
+ let(:operation) { Fabricate(:operation, resource: database) }
446
+
447
+ it 'deprovisions a database' do
448
+ expect(database).to receive(:create_operation!)
449
+ .with(type: 'deprovision').and_return(operation)
450
+
451
+ expect(subject).not_to receive(:attach_to_operation_logs)
452
+
453
+ subject.send('db:deprovision', handle)
454
+ end
455
+ end
456
+
457
+ describe '#db:versions' do
458
+ let(:token) { double('token') }
459
+
460
+ before do
461
+ allow(subject).to receive(:save_token)
462
+ allow(subject).to receive(:fetch_token) { token }
463
+ end
464
+
465
+ let(:i1) do
466
+ Fabricate(:database_image, type: 'postgresql', version: '9.4')
467
+ end
468
+
469
+ let(:i2) do
470
+ Fabricate(:database_image, type: 'postgresql', version: '10')
471
+ end
472
+
473
+ let(:i3) do
474
+ Fabricate(:database_image, type: 'redis', version: '3.0')
475
+ end
476
+
477
+ before do
478
+ allow(Aptible::Api::DatabaseImage).to receive(:all)
479
+ .with(token: token).and_return([i1, i2, i3])
480
+ end
481
+
482
+ it 'lists grouped existing Database versions' do
483
+ subject.send('db:versions')
484
+
485
+ expected = [
486
+ '=== postgresql',
487
+ '9.4',
488
+ '10',
489
+ '',
490
+ '=== redis',
491
+ '3.0',
492
+ ''
493
+ ].join("\n")
494
+
495
+ expect(captured_output_text).to eq(expected)
496
+ end
497
+ end
406
498
  end