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
@@ -18,7 +18,6 @@ end
18
18
 
19
19
  describe Aptible::CLI::Agent do
20
20
  before do
21
- allow(subject).to receive(:ask)
22
21
  allow(subject).to receive(:save_token)
23
22
  allow(subject).to receive(:attach_to_operation_logs)
24
23
  allow(subject).to receive(:fetch_token) { double 'token' }
@@ -29,6 +28,135 @@ describe Aptible::CLI::Agent do
29
28
  let!(:service) { Fabricate(:service, app: app, process_type: 'web') }
30
29
  let(:op) { Fabricate(:operation, status: 'succeeded', resource: app) }
31
30
 
31
+ describe '#apps' do
32
+ it 'lists an app in an account' do
33
+ allow(Aptible::Api::Account).to receive(:all).and_return([account])
34
+ subject.send('apps')
35
+
36
+ expect(captured_output_text)
37
+ .to eq("=== #{account.handle}\n#{app.handle}\n")
38
+ end
39
+
40
+ it 'lists multiple apps in an account' do
41
+ allow(Aptible::Api::Account).to receive(:all).and_return([account])
42
+ app2 = Fabricate(:app, handle: 'foobar', account: account)
43
+ subject.send('apps')
44
+
45
+ expect(captured_output_text)
46
+ .to eq("=== #{account.handle}\n#{app.handle}\n#{app2.handle}\n")
47
+ end
48
+
49
+ it 'lists multiple apps, grouped by account in text output' do
50
+ account1 = Fabricate(:account, handle: 'Aaccount1')
51
+ app11 = Fabricate(:app, account: account1, handle: 'app11')
52
+
53
+ account2 = Fabricate(:account, handle: 'Baccount2')
54
+ app21 = Fabricate(:app, account: account2, handle: 'app21')
55
+ app22 = Fabricate(:app, account: account2, handle: 'app21')
56
+
57
+ allow(Aptible::Api::Account).to receive(:all)
58
+ .and_return([account1, account2])
59
+
60
+ subject.send('apps')
61
+
62
+ expected_text = [
63
+ "=== #{account1.handle}",
64
+ app11.handle,
65
+ '',
66
+ "=== #{account2.handle}",
67
+ app21.handle,
68
+ app22.handle,
69
+ ''
70
+ ].join("\n")
71
+
72
+ expect(captured_output_text).to eq(expected_text)
73
+ end
74
+
75
+ it 'lists filters down to one account' do
76
+ account2 = Fabricate(:account, handle: 'account2')
77
+ app2 = Fabricate(:app, account: account2, handle: 'app2')
78
+ allow(subject).to receive(:options)
79
+ .and_return(environment: account2.handle)
80
+
81
+ allow(Aptible::Api::Account).to receive(:all)
82
+ .and_return([account, account2])
83
+ subject.send('apps')
84
+
85
+ expect(captured_output_text)
86
+ .to eq("=== #{account2.handle}\n#{app2.handle}\n")
87
+ end
88
+
89
+ it 'includes services in JSON' do
90
+ account = Fabricate(:account, handle: 'account')
91
+ app = Fabricate(:app, account: account, handle: 'app')
92
+ allow(Aptible::Api::Account).to receive(:all).and_return([account])
93
+
94
+ s1 = Fabricate(
95
+ :service,
96
+ app: app, process_type: 's1', command: 'true', container_count: 2
97
+ )
98
+ s2 = Fabricate(
99
+ :service,
100
+ app: app, process_type: 's2', container_memory_limit_mb: 2048
101
+ )
102
+
103
+ expected_json = [
104
+ {
105
+ 'environment' => {
106
+ 'id' => account.id,
107
+ 'handle' => account.handle
108
+ },
109
+ 'handle' => app.handle,
110
+ 'id' => app.id,
111
+ 'status' => app.status,
112
+ 'git_remote' => app.git_repo,
113
+ 'services' => [
114
+ {
115
+ 'service' => s1.process_type,
116
+ 'id' => s1.id,
117
+ 'command' => s1.command,
118
+ 'container_count' => s1.container_count,
119
+ 'container_size' => s1.container_memory_limit_mb
120
+ },
121
+ {
122
+ 'service' => s2.process_type,
123
+ 'id' => s2.id,
124
+ 'command' => 'CMD',
125
+ 'container_count' => s2.container_count,
126
+ 'container_size' => s2.container_memory_limit_mb
127
+ }
128
+ ]
129
+ }
130
+ ]
131
+
132
+ subject.send('apps')
133
+
134
+ expect(captured_output_json).to eq(expected_json)
135
+ end
136
+ end
137
+
138
+ describe '#apps:create' do
139
+ before do
140
+ allow(Aptible::Api::Account).to receive(:all) { [account] }
141
+ end
142
+
143
+ it 'creates an app' do
144
+ expect(account).to receive(:create_app)
145
+ .with(handle: 'foo').and_return(app)
146
+
147
+ subject.send('apps:create', 'foo')
148
+ end
149
+
150
+ it 're-raises errors' do
151
+ app.errors.full_messages << 'oops'
152
+ expect(account).to receive(:create_app)
153
+ .with(handle: 'foo').and_return(app)
154
+
155
+ expect { subject.send('apps:create', 'foo') }
156
+ .to raise_error(Thor::Error, /oops/i)
157
+ end
158
+ end
159
+
32
160
  describe '#apps:scale' do
33
161
  before do
34
162
  allow(Aptible::Api::App).to receive(:all) { [app] }
@@ -54,29 +182,29 @@ describe Aptible::CLI::Agent do
54
182
 
55
183
  it 'should scale container size and count together' do
56
184
  stub_options(container_count: 3, container_size: 1024)
57
- expect($stderr).not_to receive(:puts)
58
185
  expect(service).to receive(:create_operation!)
59
186
  .with(type: 'scale', container_count: 3, container_size: 1024)
60
187
  .and_return(op)
61
188
  subject.send('apps:scale', 'web')
189
+ expect(captured_logs).not_to match(/deprecated/i)
62
190
  end
63
191
 
64
192
  it 'should scale container count alone' do
65
193
  stub_options(container_count: 3)
66
- expect($stderr).not_to receive(:puts)
67
194
  expect(service).to receive(:create_operation!)
68
195
  .with(type: 'scale', container_count: 3)
69
196
  .and_return(op)
70
197
  subject.send('apps:scale', 'web')
198
+ expect(captured_logs).not_to match(/deprecated/i)
71
199
  end
72
200
 
73
201
  it 'should scale container size alone' do
74
202
  stub_options(container_size: 1024)
75
- expect($stderr).not_to receive(:puts)
76
203
  expect(service).to receive(:create_operation!)
77
204
  .with(type: 'scale', container_size: 1024)
78
205
  .and_return(op)
79
206
  subject.send('apps:scale', 'web')
207
+ expect(captured_logs).not_to match(/deprecated/i)
80
208
  end
81
209
 
82
210
  it 'should fail if neither container_count nor container_size is set' do
@@ -87,20 +215,20 @@ describe Aptible::CLI::Agent do
87
215
 
88
216
  it 'should scale container count (legacy)' do
89
217
  stub_options
90
- expect($stderr).to receive(:puts).once
91
218
  expect(service).to receive(:create_operation!)
92
219
  .with(type: 'scale', container_count: 3)
93
220
  .and_return(op)
94
221
  subject.send('apps:scale', 'web', '3')
222
+ expect(captured_logs).to match(/deprecated/i)
95
223
  end
96
224
 
97
225
  it 'should scale container size (legacy)' do
98
226
  stub_options(size: 90210)
99
- expect($stderr).to receive(:puts).once
100
227
  expect(service).to receive(:create_operation!)
101
228
  .with(type: 'scale', container_size: 90210)
102
229
  .and_return(op)
103
230
  subject.send('apps:scale', 'web')
231
+ expect(captured_logs).to match(/deprecated/i)
104
232
  end
105
233
 
106
234
  it 'should fail when using both current and legacy count' do
@@ -157,41 +285,29 @@ describe Aptible::CLI::Agent do
157
285
  end
158
286
 
159
287
  it 'should fail if number is not a valid number (legacy)' do
160
- expect($stderr).to receive(:puts).once
161
288
  allow(subject).to receive(:options) { { app: 'hello' } }
162
289
  allow(service).to receive(:create_operation) { op }
163
290
 
164
291
  expect do
165
292
  subject.send('apps:scale', 'web', 'potato')
166
293
  end.to raise_error(ArgumentError)
167
- end
168
- end
169
294
 
170
- describe '#config:set' do
171
- before do
172
- allow(Aptible::Api::App).to receive(:all) { [app] }
173
- allow(Aptible::Api::Account).to receive(:all) { [account] }
295
+ expect(captured_logs).to match(/deprecated/i)
174
296
  end
297
+ end
175
298
 
176
- it 'should reject environment variables that start with -' do
177
- allow(subject).to receive(:options) { { app: 'hello' } }
299
+ describe '#apps:deprovision' do
300
+ let(:operation) { Fabricate(:operation, resource: app) }
178
301
 
179
- expect { subject.send('config:set', '-foo=bar') }
180
- .to raise_error(/invalid argument/im)
181
- end
182
- end
302
+ before { allow(subject).to receive(:ensure_app).and_return(app) }
183
303
 
184
- describe '#config:rm' do
185
- before do
186
- allow(Aptible::Api::App).to receive(:all) { [app] }
187
- allow(Aptible::Api::Account).to receive(:all) { [account] }
188
- end
304
+ it 'deprovisions an app' do
305
+ expect(app).to receive(:create_operation!)
306
+ .with(type: 'deprovision').and_return(operation)
189
307
 
190
- it 'should reject environment variables that start with -' do
191
- allow(subject).to receive(:options) { { app: 'hello' } }
308
+ expect(subject).not_to receive(:attach_to_operation_logs)
192
309
 
193
- expect { subject.send('config:rm', '-foo') }
194
- .to raise_error(/invalid argument/im)
310
+ subject.send('apps:deprovision')
195
311
  end
196
312
  end
197
313
 
@@ -7,21 +7,23 @@ describe Aptible::CLI::Agent do
7
7
  let(:database) { Fabricate(:database, account: account, handle: 'some-db') }
8
8
  let!(:backup) do
9
9
  # created_at: 2016-06-14 13:24:11 +0000
10
- Fabricate(:backup, database: database, created_at: Time.at(1465910651))
10
+ Fabricate(
11
+ :backup,
12
+ database: database, created_at: Time.at(1465910651), account: account
13
+ )
11
14
  end
12
15
 
13
- let(:messages) { [] }
16
+ let(:default_handle) { 'some-db-at-2016-06-14-13-24-11' }
14
17
 
15
18
  before do
16
19
  allow(subject).to receive(:fetch_token).and_return(token)
17
- allow(subject).to receive(:say) { |m| messages << m }
18
20
  allow(Aptible::Api::Account).to receive(:all) { [account, alt_account] }
19
21
  end
20
22
 
21
23
  describe '#backup:restore' do
22
24
  it 'fails if the backup cannot be found' do
23
- expect(Aptible::Api::Backup).to receive(:find).with(1, token: token)
24
- .and_return(nil)
25
+ expect(Aptible::Api::Backup).to receive(:find)
26
+ .with(1, token: token).and_return(nil)
25
27
 
26
28
  expect { subject.send('backup:restore', 1) }
27
29
  .to raise_error('Backup #1 not found')
@@ -31,23 +33,26 @@ describe Aptible::CLI::Agent do
31
33
  let(:op) { Fabricate(:operation, resource: backup) }
32
34
 
33
35
  before do
34
- expect(Aptible::Api::Backup).to receive(:find).with(1, token: token)
35
- .and_return(backup)
36
- expect(subject).to receive(:attach_to_operation_logs).with(op)
36
+ expect(Aptible::Api::Backup).to receive(:find)
37
+ .with(1, token: token).and_return(backup)
37
38
  end
38
39
 
39
40
  it 'provides a default handle and no disk size' do
40
- h = 'some-db-at-2016-06-14-13-24-11'
41
-
42
41
  expect(backup).to receive(:create_operation!) do |options|
43
- expect(options[:handle]).to eq(h)
42
+ expect(options[:handle]).to eq(default_handle)
44
43
  expect(options[:disk_size]).not_to be_present
45
44
  expect(options[:destination_account]).not_to be_present
46
45
  op
47
46
  end
48
47
 
48
+ expect(subject).to receive(:attach_to_operation_logs).with(op) do
49
+ Fabricate(:database, account: account, handle: default_handle)
50
+ end
51
+
49
52
  subject.send('backup:restore', 1)
50
- expect(messages).to eq(["Restoring backup into #{h}"])
53
+
54
+ expect(captured_logs)
55
+ .to match(/restoring backup into #{default_handle}/im)
51
56
  end
52
57
 
53
58
  it 'accepts a handle' do
@@ -61,9 +66,13 @@ describe Aptible::CLI::Agent do
61
66
  op
62
67
  end
63
68
 
69
+ expect(subject).to receive(:attach_to_operation_logs).with(op) do
70
+ Fabricate(:database, account: account, handle: h)
71
+ end
72
+
64
73
  subject.options = { handle: h }
65
74
  subject.send('backup:restore', 1)
66
- expect(messages).to eq(["Restoring backup into #{h}"])
75
+ expect(captured_logs).to match(/restoring backup into #{h}/im)
67
76
  end
68
77
 
69
78
  it 'accepts a container size' do
@@ -77,6 +86,10 @@ describe Aptible::CLI::Agent do
77
86
  op
78
87
  end
79
88
 
89
+ expect(subject).to receive(:attach_to_operation_logs).with(op) do
90
+ Fabricate(:database, account: account, handle: default_handle)
91
+ end
92
+
80
93
  subject.options = { container_size: s }
81
94
  subject.send('backup:restore', 1)
82
95
  end
@@ -92,6 +105,10 @@ describe Aptible::CLI::Agent do
92
105
  op
93
106
  end
94
107
 
108
+ expect(subject).to receive(:attach_to_operation_logs).with(op) do
109
+ Fabricate(:database, account: account, handle: default_handle)
110
+ end
111
+
95
112
  subject.options = { size: s }
96
113
  subject.send('backup:restore', 1)
97
114
  end
@@ -103,6 +120,10 @@ describe Aptible::CLI::Agent do
103
120
  op
104
121
  end
105
122
 
123
+ expect(subject).to receive(:attach_to_operation_logs).with(op) do
124
+ Fabricate(:database, account: alt_account, handle: default_handle)
125
+ end
126
+
106
127
  subject.options = { environment: 'alt' }
107
128
  subject.send('backup:restore', 1)
108
129
  end
@@ -131,19 +152,19 @@ describe Aptible::CLI::Agent do
131
152
 
132
153
  it 'can show a subset of backups' do
133
154
  subject.send('backup:list', database.handle)
134
- expect(messages.size).to eq(5)
155
+ expect(captured_output_text.split("\n").size).to eq(5)
135
156
  end
136
157
 
137
158
  it 'allows scoping via environment' do
138
159
  subject.options = { max_age: '1w', environment: database.account.handle }
139
160
  subject.send('backup:list', database.handle)
140
- expect(messages.size).to eq(5)
161
+ expect(captured_output_text.split("\n").size).to eq(5)
141
162
  end
142
163
 
143
164
  it 'shows more backups if requested' do
144
165
  subject.options = { max_age: '2y' }
145
166
  subject.send('backup:list', database.handle)
146
- expect(messages.size).to eq(9)
167
+ expect(captured_output_text.split("\n").size).to eq(9)
147
168
  end
148
169
 
149
170
  it 'errors out if max_age is invalid' do
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Agent do
4
+ let(:account) { Fabricate(:account) }
5
+ let(:app) { Fabricate(:app, account: account) }
6
+
7
+ let(:token) { double('token') }
8
+ before { allow(subject).to receive(:fetch_token).and_return(token) }
9
+
10
+ before do
11
+ allow(Aptible::Api::App).to receive(:all)
12
+ .with(token: token).and_return([app])
13
+ allow(Aptible::Api::Account).to receive(:all)
14
+ .with(token: token).and_return([account])
15
+ end
16
+
17
+ before { allow(subject).to receive(:options) { { app: app.handle } } }
18
+ let(:operation) { Fabricate(:operation, resource: app) }
19
+
20
+ describe '#config' do
21
+ before { allow(subject).to receive(:options).and_return(app: app.handle) }
22
+
23
+ it 'shows nothing for an unconfigured app' do
24
+ subject.send('config')
25
+ expect(captured_output_text).to eq('')
26
+ expect(captured_output_json).to match_array([])
27
+ end
28
+
29
+ it 'shows an empty configuration' do
30
+ app.current_configuration = Fabricate(:configuration, app: app)
31
+ subject.send('config')
32
+ expect(captured_output_text).to eq('')
33
+ expect(captured_output_json).to match_array([])
34
+ end
35
+
36
+ it 'should show environment variables' do
37
+ app.current_configuration = Fabricate(
38
+ :configuration, app: app, env: { 'FOO' => 'BAR', 'QUX' => 'two words' }
39
+ )
40
+ subject.send('config')
41
+
42
+ expect(captured_output_text).to match(/FOO=BAR/)
43
+ expect(captured_output_text).to match(/QUX=two\\ words/)
44
+
45
+ expected = [
46
+ {
47
+ 'key' => 'FOO', 'value' => 'BAR',
48
+ 'shell_export' => 'FOO=BAR'
49
+ },
50
+ {
51
+ 'key' => 'QUX', 'value' => 'two words',
52
+ 'shell_export' => 'QUX=two\\ words'
53
+ }
54
+ ]
55
+
56
+ expect(captured_output_json).to match_array(expected)
57
+ end
58
+ end
59
+
60
+ describe '#config:set' do
61
+ it 'sets environment variables' do
62
+ expect(app).to receive(:create_operation!)
63
+ .with(type: 'configure', env: { 'FOO' => 'BAR' })
64
+ .and_return(operation)
65
+
66
+ expect(subject).to receive(:attach_to_operation_logs)
67
+ .with(operation)
68
+
69
+ subject.send('config:set', 'FOO=BAR')
70
+ end
71
+
72
+ it 'rejects environment variables that start with -' do
73
+ expect { subject.send('config:set', '-foo=bar') }
74
+ .to raise_error(/invalid argument/im)
75
+ end
76
+ end
77
+
78
+ describe '#config:rm' do
79
+ it 'unsets environment variables' do
80
+ expect(app).to receive(:create_operation!)
81
+ .with(type: 'configure', env: { 'FOO' => '' })
82
+ .and_return(operation)
83
+
84
+ expect(subject).to receive(:attach_to_operation_logs)
85
+ .with(operation)
86
+
87
+ subject.send('config:unset', 'FOO')
88
+ end
89
+
90
+ it 'rejects environment variables that start with -' do
91
+ expect { subject.send('config:rm', '-foo') }
92
+ .to raise_error(/invalid argument/im)
93
+ end
94
+ end
95
+ end