aptible-cli 0.14.1 → 0.15.0

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