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