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
@@ -36,7 +36,7 @@ module Aptible
36
36
  end
37
37
 
38
38
  operation = app.create_operation!(type: type)
39
- puts 'Restarting app...'
39
+ CLI.logger.info 'Restarting app...'
40
40
  attach_to_operation_logs(operation)
41
41
  end
42
42
  end
@@ -11,15 +11,14 @@ module Aptible
11
11
  def services
12
12
  app = ensure_app(options)
13
13
 
14
- first = true
15
- app.each_service do |service|
16
- say '' unless first
17
- first = false
18
-
19
- say "Service: #{service.process_type}"
20
- say "Command: #{service.command || 'CMD'}"
21
- say "Container Count: #{service.container_count}"
22
- say "Container Size: #{service.container_memory_limit_mb}"
14
+ Formatter.render(Renderer.current) do |root|
15
+ root.list do |list|
16
+ app.each_service do |service|
17
+ list.object do |node|
18
+ ResourceFormatter.inject_service(node, service, app)
19
+ end
20
+ end
21
+ end
23
22
  end
24
23
  end
25
24
  end
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.14.1'.freeze
3
+ VERSION = '0.15.0'.freeze
4
4
  end
5
5
  end
@@ -11,16 +11,16 @@ describe Aptible::CLI::Agent do
11
11
  it 'should print the version' do
12
12
  ClimateControl.modify(APTIBLE_TOOLBELT: nil) do
13
13
  version = Aptible::CLI::VERSION
14
- expect(STDOUT).to receive(:puts).with "aptible-cli v#{version}"
15
14
  subject.version
15
+ expect(captured_output_text).to eq("aptible-cli v#{version}\n")
16
16
  end
17
17
  end
18
18
 
19
19
  it 'should print the version (with toolbelt)' do
20
20
  ClimateControl.modify(APTIBLE_TOOLBELT: '1') do
21
21
  version = Aptible::CLI::VERSION
22
- expect(STDOUT).to receive(:puts).with "aptible-cli v#{version} toolbelt"
23
22
  subject.version
23
+ expect(captured_output_text).to eq("aptible-cli v#{version} toolbelt\n")
24
24
  end
25
25
  end
26
26
  end
@@ -29,7 +29,6 @@ describe Aptible::CLI::Agent do
29
29
  let(:token) { double('Aptible::Auth::Token') }
30
30
  let(:created_at) { Time.now }
31
31
  let(:expires_at) { created_at + 1.week }
32
- let(:output) { [] }
33
32
 
34
33
  before do
35
34
  m = -> (code) { @code = code }
@@ -38,7 +37,7 @@ describe Aptible::CLI::Agent do
38
37
  allow(token).to receive(:access_token).and_return 'access_token'
39
38
  allow(token).to receive(:created_at).and_return created_at
40
39
  allow(token).to receive(:expires_at).and_return expires_at
41
- allow(subject).to receive(:puts) { |v| output << v }
40
+ allow(subject).to receive(:puts) {}
42
41
  end
43
42
 
44
43
  it 'should save a token to ~/.aptible/tokens' do
@@ -50,10 +49,8 @@ describe Aptible::CLI::Agent do
50
49
  it 'should output the token location and token lifetime' do
51
50
  allow(Aptible::Auth::Token).to receive(:create).and_return token
52
51
  subject.login
53
- expect(output.size).to eq(3)
54
- expect(output[0]).to eq('')
55
- expect(output[1]).to match(/written to some\.json/)
56
- expect(output[2]).to match(/will expire after 7 days/)
52
+ expect(captured_logs).to match(/token written to.*json/i)
53
+ expect(captured_logs).to match(/expire after 7 days/i)
57
54
  end
58
55
 
59
56
  it 'should raise an error if authentication fails' do
@@ -181,9 +178,9 @@ describe Aptible::CLI::Agent do
181
178
  let(:nag_file) { File.join(nag_dir, 'nag_toolbelt') }
182
179
 
183
180
  it 'warns if the nag file is not present' do
184
- expect($stderr).to receive(:puts).at_least(:once)
185
181
  subject.send(:nag_toolbelt)
186
182
  expect(Integer(File.read(nag_file))).to be_within(5).of(Time.now.utc.to_i)
183
+ expect(captured_logs).to match(/from source/)
187
184
  end
188
185
 
189
186
  it 'warns if the nag file contains an old timestamp' do
@@ -192,8 +189,8 @@ describe Aptible::CLI::Agent do
192
189
  f.write((Time.now.utc.to_i - 1.day).to_i.to_s)
193
190
  end
194
191
 
195
- expect($stderr).to receive(:puts).at_least(:once)
196
192
  subject.send(:nag_toolbelt)
193
+ expect(captured_logs).to match(/from source/)
197
194
  end
198
195
 
199
196
  it 'does not warn if the nag file contains a recent timestamp' do
@@ -202,8 +199,8 @@ describe Aptible::CLI::Agent do
202
199
  f.write((Time.now.utc.to_i - 3.hours).to_i.to_s)
203
200
  end
204
201
 
205
- expect($stderr).not_to receive(:puts)
206
202
  subject.send(:nag_toolbelt)
203
+ expect(captured_logs).to eq('')
207
204
  end
208
205
 
209
206
  it 'does not warn if the nag file contains a recent timestamp (newline)' do
@@ -214,21 +211,21 @@ describe Aptible::CLI::Agent do
214
211
  f.write("#{(Time.now.utc.to_i - 3.hours).to_i}\n")
215
212
  end
216
213
 
217
- expect($stderr).not_to receive(:puts)
218
214
  subject.send(:nag_toolbelt)
215
+ expect(captured_logs).to eq('')
219
216
  end
220
217
 
221
218
  it 'warns if the nag file contains an invalid timestamp' do
222
219
  Dir.mkdir(nag_dir)
223
220
  File.open(nag_file, 'w') { |f| f.write('foobar') }
224
221
 
225
- expect($stderr).to receive(:puts).at_least(:once)
226
222
  subject.send(:nag_toolbelt)
223
+ expect(captured_logs).to match(/from source/)
227
224
  end
228
225
 
229
226
  it 'is compatible with itself' do
230
- expect($stderr).to receive(:puts).once
231
227
  2.times { subject.send(:nag_toolbelt) }
228
+ expect(captured_logs.split("\n").grep(/from source/).size).to eq(1)
232
229
  end
233
230
  end
234
231
 
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Formatter do
4
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Renderer::Json do
4
+ subject { described_class.new }
5
+
6
+ let(:root) { Aptible::CLI::Formatter::Root.new }
7
+
8
+ it 'renders an object' do
9
+ root.object { |n| n.value('foo', 'bar') }
10
+ expect(JSON.parse(subject.render(root))).to eq('foo' => 'bar')
11
+ end
12
+
13
+ it 'renders a list' do
14
+ root.list do |l|
15
+ l.value('foo')
16
+ l.value('bar')
17
+ end
18
+ expect(JSON.parse(subject.render(root))).to eq(%w(foo bar))
19
+ end
20
+
21
+ it 'ignores keyed_list' do
22
+ root.keyed_list('foo') do |l|
23
+ l.object do |n|
24
+ n.value('foo', 'bar')
25
+ n.value('qux', 'baz')
26
+ end
27
+ end
28
+
29
+ expect(JSON.parse(subject.render(root)))
30
+ .to eq([{ 'foo' => 'bar', 'qux' => 'baz' }])
31
+ end
32
+
33
+ it 'ignores grouped_keyed_list' do
34
+ root.grouped_keyed_list('foo', 'qux') do |l|
35
+ l.object do |n|
36
+ n.value('foo', 'bar')
37
+ n.value('qux', 'baz')
38
+ end
39
+ end
40
+
41
+ expect(JSON.parse(subject.render(root)))
42
+ .to eq([{ 'foo' => 'bar', 'qux' => 'baz' }])
43
+ end
44
+
45
+ it 'ignores keyed_object' do
46
+ root.keyed_object('foo') { |n| n.value('foo', 'bar') }
47
+ expect(JSON.parse(subject.render(root))).to eq('foo' => 'bar')
48
+ end
49
+
50
+ it 'nests objects' do
51
+ root.object do |n|
52
+ n.object('foo') { |nn| nn.value('bar', 'qux') }
53
+ end
54
+ expect(JSON.parse(subject.render(root))).to eq('foo' => { 'bar' => 'qux' })
55
+ end
56
+
57
+ it 'nests lists' do
58
+ root.list do |n|
59
+ n.list { |nn| nn.value('bar') }
60
+ end
61
+ expect(JSON.parse(subject.render(root))).to eq([%w(bar)])
62
+ end
63
+ end
@@ -0,0 +1,150 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Renderer::Text do
4
+ subject { described_class.new }
5
+
6
+ let(:root) { Aptible::CLI::Formatter::Root.new }
7
+
8
+ it 'renders an object' do
9
+ root.object { |n| n.value('foo', 'bar') }
10
+ expect(subject.render(root)).to eq("Foo: bar\n")
11
+ end
12
+
13
+ it 'renders a list' do
14
+ root.list do |l|
15
+ l.value('foo')
16
+ l.value('bar')
17
+ end
18
+ expect(subject.render(root)).to eq("foo\nbar\n")
19
+ end
20
+
21
+ it 'renders the key in a keyed_list' do
22
+ root.keyed_list('foo') do |l|
23
+ l.object do |n|
24
+ n.value('foo', 'bar')
25
+ n.value('qux', 'baz')
26
+ end
27
+
28
+ l.object do |n|
29
+ n.value('foo', 'bar2')
30
+ n.value('qux', 'baz2')
31
+ end
32
+ end
33
+
34
+ expect(subject.render(root)).to eq("bar\nbar2\n")
35
+ end
36
+
37
+ it 'renders the keys in a grouped_keyed_list, with plain grouping' do
38
+ root.grouped_keyed_list('foo', 'qux') do |l|
39
+ l.object do |n|
40
+ n.value('foo', 'bar')
41
+ n.value('qux', 'baz')
42
+ end
43
+ l.object do |n|
44
+ n.value('foo', 'bar')
45
+ n.value('qux', 'baz2')
46
+ end
47
+ l.object do |n|
48
+ n.value('foo', 'bar2')
49
+ n.value('qux', 'baz3')
50
+ end
51
+ end
52
+
53
+ expected = [
54
+ '=== bar',
55
+ 'baz',
56
+ 'baz2',
57
+ '',
58
+ '=== bar2',
59
+ 'baz3',
60
+ ''
61
+ ].join("\n")
62
+
63
+ expect(subject.render(root)).to eq(expected)
64
+ end
65
+
66
+ it 'renders the keys in a grouped_keyed_list, with nested grouping' do
67
+ root.grouped_keyed_list({ 'foo' => 'nest' }, 'qux') do |l|
68
+ l.object do |n|
69
+ n.object('foo') { |nn| nn.value('nest', 'bar') }
70
+ n.value('qux', 'baz')
71
+ end
72
+ l.object do |n|
73
+ n.object('foo') { |nn| nn.value('nest', 'bar') }
74
+ n.value('qux', 'baz2')
75
+ end
76
+ l.object do |n|
77
+ n.object('foo') { |nn| nn.value('nest', 'bar2') }
78
+ n.value('qux', 'baz3')
79
+ end
80
+ end
81
+
82
+ expected = [
83
+ '=== bar',
84
+ 'baz',
85
+ 'baz2',
86
+ '',
87
+ '=== bar2',
88
+ 'baz3',
89
+ ''
90
+ ].join("\n")
91
+
92
+ expect(subject.render(root)).to eq(expected)
93
+ end
94
+
95
+ it 'renders the key in a keyed_object' do
96
+ root.keyed_object('foo') { |n| n.value('foo', 'bar') }
97
+ expect(subject.render(root)).to eq("bar\n")
98
+ end
99
+
100
+ it 'renders a plain value' do
101
+ root.value('foo')
102
+ expect(subject.render(root)).to eq("foo\n")
103
+ end
104
+
105
+ it 'renders a list of objects' do
106
+ root.list do |l|
107
+ l.object do |n|
108
+ n.value('foo', 'bar1')
109
+ n.value('qux', 'baz1')
110
+ end
111
+ l.object do |n|
112
+ n.value('foo', 'bar2')
113
+ n.value('qux', 'baz2')
114
+ end
115
+ l.object do |n|
116
+ n.value('foo', 'bar3')
117
+ n.value('qux', 'baz3')
118
+ end
119
+ end
120
+
121
+ expected = [
122
+ 'Foo: bar1',
123
+ 'Qux: baz1',
124
+ '',
125
+ 'Foo: bar2',
126
+ 'Qux: baz2',
127
+ '',
128
+ 'Foo: bar3',
129
+ 'Qux: baz3',
130
+ ''
131
+ ].join("\n")
132
+
133
+ expect(subject.render(root)).to eq(expected)
134
+ end
135
+
136
+ it 'capitalizes keys' do
137
+ root.list do |l|
138
+ l.object do |n|
139
+ n.value('this is tls dns and ip', 'foo')
140
+ end
141
+ end
142
+
143
+ expected = [
144
+ 'This Is TLS DNS And IP: foo',
145
+ ''
146
+ ].join("\n")
147
+
148
+ expect(subject.render(root)).to eq(expected)
149
+ end
150
+ end
@@ -0,0 +1,113 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::ResourceFormatter do
4
+ def capture(m, *args)
5
+ node = Aptible::CLI::Formatter::Object.new
6
+ described_class.public_send(m, node, *args)
7
+ Aptible::CLI::Renderer::Text.new.render(node).split("\n")
8
+ end
9
+
10
+ describe '#inject_vhost' do
11
+ it 'explains a VHOST' do
12
+ service = Fabricate(:service, process_type: 'web')
13
+ vhost = Fabricate(
14
+ :vhost,
15
+ id: 12,
16
+ service: service,
17
+ external_host: 'foo.io',
18
+ status: 'provisioned',
19
+ type: 'http_proxy_protocol',
20
+ internal: false,
21
+ ip_whitelist: [],
22
+ default: false,
23
+ acme: false
24
+ )
25
+
26
+ expected = [
27
+ 'Id: 12',
28
+ 'Hostname: foo.io',
29
+ 'Status: provisioned',
30
+ 'Type: https',
31
+ 'Port: default',
32
+ 'Internal: false',
33
+ 'IP Whitelist: all traffic',
34
+ 'Default Domain Enabled: false',
35
+ 'Managed TLS Enabled: false',
36
+ 'Service: web'
37
+ ]
38
+ expect(capture(:inject_vhost, vhost, service)).to eq(expected)
39
+ end
40
+
41
+ it 'explains a failed VHOST' do
42
+ vhost = Fabricate(:vhost, status: 'provision_failed')
43
+ expect(capture(:inject_vhost, vhost, vhost.service))
44
+ .to include('Status: provision_failed')
45
+ end
46
+
47
+ it 'explains an internal VHOST' do
48
+ vhost = Fabricate(:vhost, internal: true)
49
+ expect(capture(:inject_vhost, vhost, vhost.service))
50
+ .to include('Internal: true')
51
+ end
52
+
53
+ it 'explains a default VHOST' do
54
+ vhost = Fabricate(:vhost, default: true, virtual_domain: 'qux.io')
55
+ expect(capture(:inject_vhost, vhost, vhost.service))
56
+ .to include('Default Domain Enabled: true')
57
+ expect(capture(:inject_vhost, vhost, vhost.service))
58
+ .to include('Default Domain: qux.io')
59
+ end
60
+
61
+ it 'explains a TLS VHOST' do
62
+ vhost = Fabricate(:vhost, type: 'tls')
63
+ expect(capture(:inject_vhost, vhost, vhost.service))
64
+ .to include('Type: tls')
65
+ expect(capture(:inject_vhost, vhost, vhost.service))
66
+ .to include('Ports: all')
67
+ end
68
+
69
+ it 'explains a TCP VHOST' do
70
+ vhost = Fabricate(:vhost, type: 'tcp')
71
+ expect(capture(:inject_vhost, vhost, vhost.service))
72
+ .to include('Type: tcp')
73
+ expect(capture(:inject_vhost, vhost, vhost.service))
74
+ .to include('Ports: all')
75
+ end
76
+
77
+ it 'explains a VHOST with a container port' do
78
+ vhost = Fabricate(:vhost, type: 'http_proxy_protocol', container_port: 12)
79
+ expect(capture(:inject_vhost, vhost, vhost.service))
80
+ .to include('Port: 12')
81
+ end
82
+
83
+ it 'explains a VHOST with container ports' do
84
+ vhost = Fabricate(:vhost, type: 'tls', container_ports: [12, 34])
85
+ expect(capture(:inject_vhost, vhost, vhost.service))
86
+ .to include('Ports: 12 34')
87
+ end
88
+
89
+ it 'explains a VHOST with IP Filtering' do
90
+ vhost = Fabricate(:vhost, ip_whitelist: %w(1.1.1.1/1 2.2.2.2/2))
91
+ expect(capture(:inject_vhost, vhost, vhost.service))
92
+ .to include('IP Whitelist: 1.1.1.1/1 2.2.2.2/2')
93
+ end
94
+
95
+ it 'explains a VHOST with Managed TLS' do
96
+ vhost = Fabricate(
97
+ :vhost,
98
+ acme: true,
99
+ user_domain: 'foo.io',
100
+ acme_dns_challenge_host: 'dns.qux.io',
101
+ acme_status: 'ready'
102
+ )
103
+ expect(capture(:inject_vhost, vhost, vhost.service))
104
+ .to include('Managed TLS Enabled: true')
105
+ expect(capture(:inject_vhost, vhost, vhost.service))
106
+ .to include('Managed TLS Domain: foo.io')
107
+ expect(capture(:inject_vhost, vhost, vhost.service))
108
+ .to include('Managed TLS DNS Challenge Hostname: dns.qux.io')
109
+ expect(capture(:inject_vhost, vhost, vhost.service))
110
+ .to include('Managed TLS Status: ready')
111
+ end
112
+ end
113
+ end