aptible-cli 0.6.9 → 0.7.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/README.md +1 -1
  4. data/aptible-cli.gemspec +6 -3
  5. data/bin/aptible +3 -1
  6. data/codecov.yml +12 -0
  7. data/lib/aptible/cli/agent.rb +3 -0
  8. data/lib/aptible/cli/helpers/app.rb +98 -33
  9. data/lib/aptible/cli/helpers/database.rb +3 -3
  10. data/lib/aptible/cli/subcommands/apps.rb +4 -3
  11. data/lib/aptible/cli/subcommands/backup.rb +55 -0
  12. data/lib/aptible/cli/subcommands/config.rb +2 -2
  13. data/lib/aptible/cli/subcommands/db.rb +11 -2
  14. data/lib/aptible/cli/subcommands/rebuild.rb +1 -1
  15. data/lib/aptible/cli/subcommands/restart.rb +1 -1
  16. data/lib/aptible/cli/version.rb +1 -1
  17. data/spec/aptible/cli/helpers/git_remote_handle_strategy_spec.rb +54 -0
  18. data/spec/aptible/cli/helpers/{handle_from_git_remote.rb → handle_from_git_remote_spec.rb} +0 -0
  19. data/spec/aptible/cli/helpers/options_handle_strategy_spec.rb +14 -0
  20. data/spec/aptible/cli/helpers/tunnel_spec.rb +0 -1
  21. data/spec/aptible/cli/subcommands/apps_spec.rb +141 -54
  22. data/spec/aptible/cli/subcommands/backup_spec.rb +115 -0
  23. data/spec/aptible/cli/subcommands/db_spec.rb +35 -61
  24. data/spec/aptible/cli/subcommands/domains_spec.rb +21 -38
  25. data/spec/aptible/cli/subcommands/logs_spec.rb +12 -17
  26. data/spec/aptible/cli/subcommands/ps_spec.rb +5 -12
  27. data/spec/fabricators/account_fabricator.rb +10 -0
  28. data/spec/fabricators/app_fabricator.rb +14 -0
  29. data/spec/fabricators/backup_fabricator.rb +10 -0
  30. data/spec/fabricators/database_fabricator.rb +15 -0
  31. data/spec/fabricators/operation_fabricator.rb +6 -0
  32. data/spec/fabricators/service_fabricator.rb +9 -0
  33. data/spec/fabricators/vhost_fabricator.rb +9 -0
  34. data/spec/spec_helper.rb +9 -1
  35. metadata +81 -17
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Helpers::App::OptionsHandleStrategy do
4
+ it 'is usable when app is set' do
5
+ s = described_class.new(app: 'foo')
6
+ expect(s.usable?).to be_truthy
7
+ end
8
+
9
+ it 'passes options through' do
10
+ s = described_class.new(app: 'foo', environment: 'bar')
11
+ expect(s.app_handle).to eq('foo')
12
+ expect(s.env_handle).to eq('bar')
13
+ end
14
+ end
@@ -1,5 +1,4 @@
1
1
  require 'spec_helper'
2
- require 'climate_control'
3
2
 
4
3
  describe Aptible::CLI::Helpers::Tunnel do
5
4
  around do |example|
@@ -1,16 +1,17 @@
1
- require 'ostruct'
2
- require 'spec_helper'
3
-
4
- class App < OpenStruct
5
- end
6
-
7
- class Service < OpenStruct
8
- end
9
-
10
- class Operation < OpenStruct
11
- end
12
-
13
- class Account < OpenStruct
1
+ def dummy_strategy_factory(app_handle, env_handle, usable,
2
+ options_receiver = [])
3
+ Class.new do
4
+ attr_reader :options
5
+
6
+ define_method(:initialize) { |options| options_receiver << options }
7
+ define_method(:app_handle) { app_handle }
8
+ define_method(:env_handle) { env_handle }
9
+ define_method(:usable?) { usable }
10
+
11
+ def explain
12
+ '(options from dummy)'
13
+ end
14
+ end
14
15
  end
15
16
 
16
17
  describe Aptible::CLI::Agent do
@@ -19,49 +20,40 @@ describe Aptible::CLI::Agent do
19
20
  before { subject.stub(:fetch_token) { double 'token' } }
20
21
  before { subject.stub(:attach_to_operation_logs) }
21
22
 
22
- let(:service) { Service.new(process_type: 'web') }
23
- let(:op) { Operation.new(status: 'succeeded') }
24
- let(:account) do
25
- Account.new(bastion_host: 'localhost',
26
- dumptruck_port: 1234,
27
- handle: 'aptible')
28
- end
29
- let(:services) { [service] }
30
- let(:apps) do
31
- [App.new(handle: 'hello', services: services, account: account)]
32
- end
23
+ let!(:account) { Fabricate(:account) }
24
+ let!(:app) { Fabricate(:app, handle: 'hello', account: account) }
25
+ let!(:service) { Fabricate(:service, app: app) }
26
+ let(:op) { Fabricate(:operation, status: 'succeeded', resource: app) }
33
27
 
34
28
  describe '#apps:scale' do
29
+ before do
30
+ allow(Aptible::Api::App).to receive(:all) { [app] }
31
+ allow(Aptible::Api::Account).to receive(:all) { [account] }
32
+ end
33
+
35
34
  it 'should pass given correct parameters' do
36
- allow(service).to receive(:create_operation) { op }
37
35
  allow(subject).to receive(:options) do
38
36
  { app: 'hello', environment: 'foobar' }
39
37
  end
40
- allow(op).to receive(:resource) { apps.first }
41
- allow(Aptible::Api::App).to receive(:all) { apps }
42
-
38
+ expect(service).to receive(:create_operation!) { op }
43
39
  expect(subject).to receive(:environment_from_handle)
44
40
  .with('foobar')
45
41
  .and_return(account)
46
- expect(subject).to receive(:apps_from_handle).and_return(apps)
42
+ expect(subject).to receive(:apps_from_handle).and_return([app])
47
43
  subject.send('apps:scale', 'web', 3)
48
44
  end
49
45
 
50
46
  it 'should pass container size param to operation if given' do
51
- expect(service).to receive(:create_operation)
52
- .with(type: 'scale', container_count: 3, container_size: 90210)
53
- .and_return(op)
54
47
  allow(subject).to receive(:options) do
55
48
  { app: 'hello', size: 90210, environment: 'foobar' }
56
49
  end
57
-
58
- allow(op).to receive(:resource) { apps.first }
59
- allow(Aptible::Api::App).to receive(:all) { apps }
60
-
50
+ expect(service).to receive(:create_operation!)
51
+ .with(type: 'scale', container_count: 3, container_size: 90210)
52
+ .and_return(op)
61
53
  expect(subject).to receive(:environment_from_handle)
62
54
  .with('foobar')
63
55
  .and_return(account)
64
- expect(subject).to receive(:apps_from_handle).and_return(apps)
56
+ expect(subject).to receive(:apps_from_handle).and_return([app])
65
57
  subject.send('apps:scale', 'web', 3)
66
58
  end
67
59
 
@@ -69,9 +61,8 @@ describe Aptible::CLI::Agent do
69
61
  allow(subject).to receive(:options) do
70
62
  { environment: 'foo', app: 'web' }
71
63
  end
72
- allow(service).to receive(:create_operation) { op }
73
64
  allow(Aptible::Api::Account).to receive(:all) { [] }
74
- allow(account).to receive(:apps) { [apps] }
65
+ allow(service).to receive(:create_operation!) { op }
75
66
 
76
67
  expect do
77
68
  subject.send('apps:scale', 'web', 3)
@@ -79,19 +70,14 @@ describe Aptible::CLI::Agent do
79
70
  end
80
71
 
81
72
  it 'should fail if app is non-existent' do
82
- allow(service).to receive(:create_operation) { op }
83
- allow(Aptible::Api::Account).to receive(:all) { [account] }
84
- allow(account).to receive(:apps) { [] }
85
-
86
73
  expect do
87
74
  subject.send('apps:scale', 'web', 3)
88
75
  end.to raise_error(Thor::Error)
89
76
  end
90
77
 
91
78
  it 'should fail if number is not a valid number' do
92
- allow(service).to receive(:create_operation) { op }
93
79
  allow(subject).to receive(:options) { { app: 'hello' } }
94
- allow(Aptible::Api::App).to receive(:all) { apps }
80
+ allow(service).to receive(:create_operation) { op }
95
81
 
96
82
  expect do
97
83
  subject.send('apps:scale', 'web', 'potato')
@@ -105,8 +91,7 @@ describe Aptible::CLI::Agent do
105
91
  expect(subject).to receive(:environment_from_handle)
106
92
  .with('foobar')
107
93
  .and_return(account)
108
- expect(subject).to receive(:apps_from_handle).and_return(apps)
109
- allow(Aptible::Api::App).to receive(:all) { apps }
94
+ expect(subject).to receive(:apps_from_handle).and_return([app])
110
95
 
111
96
  expect do
112
97
  subject.send('apps:scale', 'potato', 1)
@@ -114,18 +99,16 @@ describe Aptible::CLI::Agent do
114
99
  end
115
100
 
116
101
  context 'no service' do
117
- let(:services) { [] }
102
+ before { app.services = [] }
118
103
 
119
104
  it 'should fail if the app has no services' do
120
- expect(subject).to receive(:environment_from_handle)
121
- .with('foobar')
122
- .and_return(account)
123
- expect(subject).to receive(:apps_from_handle).and_return(apps)
124
105
  allow(subject).to receive(:options) do
125
106
  { app: 'hello', environment: 'foobar' }
126
107
  end
127
-
128
- allow(Aptible::Api::App).to receive(:all) { apps }
108
+ expect(subject).to receive(:environment_from_handle)
109
+ .with('foobar')
110
+ .and_return(account)
111
+ expect(subject).to receive(:apps_from_handle).and_return([app])
129
112
 
130
113
  expect do
131
114
  subject.send('apps:scale', 'web', 1)
@@ -133,4 +116,108 @@ describe Aptible::CLI::Agent do
133
116
  end
134
117
  end
135
118
  end
119
+
120
+ describe '#ensure_app' do
121
+ it 'fails if no usable strategy is found' do
122
+ strategies = [dummy_strategy_factory(nil, nil, false)]
123
+ subject.stub(:handle_strategies) { strategies }
124
+
125
+ expect { subject.ensure_app }.to raise_error(/Could not find app/)
126
+ end
127
+
128
+ it 'fails if an environment is specified but not found' do
129
+ strategies = [dummy_strategy_factory('hello', 'aptible', true)]
130
+ subject.stub(:handle_strategies) { strategies }
131
+
132
+ expect(subject).to receive(:environment_from_handle).and_return(nil)
133
+
134
+ expect { subject.ensure_app }.to raise_error(/Could not find environment/)
135
+ end
136
+
137
+ context 'with apps' do
138
+ let(:apps) { [app] }
139
+
140
+ before do
141
+ account.apps = apps
142
+ allow(Aptible::Api::App).to receive(:all).and_return(apps)
143
+ end
144
+
145
+ it 'scopes the app search to an environment if provided' do
146
+ strategies = [dummy_strategy_factory('hello', 'aptible', true)]
147
+ subject.stub(:handle_strategies) { strategies }
148
+
149
+ expect(subject).to receive(:environment_from_handle).with('aptible')
150
+ .and_return(account)
151
+
152
+ expect(subject.ensure_app).to eq(apps.first)
153
+ end
154
+
155
+ it 'does not scope the app search to an environment if not provided' do
156
+ strategies = [dummy_strategy_factory('hello', nil, true)]
157
+ subject.stub(:handle_strategies) { strategies }
158
+
159
+ expect(subject.ensure_app).to eq(apps.first)
160
+ end
161
+
162
+ it 'fails if no app is found' do
163
+ apps.pop
164
+
165
+ strategies = [dummy_strategy_factory('hello', nil, true)]
166
+ subject.stub(:handle_strategies) { strategies }
167
+
168
+ expect { subject.ensure_app }.to raise_error(/not find app hello/)
169
+ end
170
+
171
+ it 'explains the strategy when it fails' do
172
+ apps.pop
173
+
174
+ strategies = [dummy_strategy_factory('hello', nil, true)]
175
+ subject.stub(:handle_strategies) { strategies }
176
+
177
+ expect { subject.ensure_app }.to raise_error(/from dummy/)
178
+ end
179
+
180
+ it 'indicates the environment when the app search was scoped' do
181
+ apps.pop
182
+
183
+ strategies = [dummy_strategy_factory('hello', 'aptible', true)]
184
+ subject.stub(:handle_strategies) { strategies }
185
+
186
+ expect(subject).to receive(:environment_from_handle).with('aptible')
187
+ .and_return(account)
188
+
189
+ expect { subject.ensure_app }.to raise_error(/in environment aptible/)
190
+ end
191
+
192
+ it 'fails if multiple apps are found' do
193
+ apps << Fabricate(:app, handle: 'hello')
194
+
195
+ strategies = [dummy_strategy_factory('hello', nil, true)]
196
+ subject.stub(:handle_strategies) { strategies }
197
+
198
+ expect { subject.ensure_app }.to raise_error(/Multiple apps/)
199
+ end
200
+
201
+ it 'falls back to another strategy when the first one is unusable' do
202
+ strategies = [
203
+ dummy_strategy_factory('hello', nil, false),
204
+ dummy_strategy_factory('hello', nil, true)
205
+ ]
206
+ subject.stub(:handle_strategies) { strategies }
207
+
208
+ expect(subject.ensure_app).to eq(apps.first)
209
+ end
210
+
211
+ it 'passes options to the strategy' do
212
+ receiver = []
213
+ strategies = [dummy_strategy_factory('hello', nil, false, receiver)]
214
+ subject.stub(:handle_strategies) { strategies }
215
+
216
+ options = { app: 'foo', environment: 'bar' }
217
+ expect { subject.ensure_app options }.to raise_error(/not find app/)
218
+
219
+ expect(receiver).to eq([options])
220
+ end
221
+ end
222
+ end
136
223
  end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Agent do
4
+ let(:token) { 'some-token' }
5
+ let(:account) { Fabricate(:account) }
6
+ let(:database) { Fabricate(:database, account: account, handle: 'some-db') }
7
+ let!(:backup) do
8
+ # created_at: 2016-06-14 13:24:11 +0000
9
+ Fabricate(:backup, database: database, created_at: Time.at(1465910651))
10
+ end
11
+
12
+ let(:messages) { [] }
13
+
14
+ before do
15
+ allow(subject).to receive(:fetch_token).and_return(token)
16
+ allow(subject).to receive(:say) { |m| messages << m }
17
+ end
18
+
19
+ describe '#backup:restore' do
20
+ it 'fails if the backup cannot be found' do
21
+ expect(Aptible::Api::Backup).to receive(:find).with(1, token: token)
22
+ .and_return(nil)
23
+
24
+ expect { subject.send('backup:restore', 1) }
25
+ .to raise_error('Backup #1 not found')
26
+ end
27
+
28
+ context 'successful restore' do
29
+ let(:op) { Fabricate(:operation, resource: backup) }
30
+
31
+ before do
32
+ expect(Aptible::Api::Backup).to receive(:find).with(1, token: token)
33
+ .and_return(backup)
34
+ expect(subject).to receive(:attach_to_operation_logs).with(op)
35
+ end
36
+
37
+ it 'provides a default handle and no disk size' do
38
+ h = 'some-db-at-2016-06-14-13-24-11'
39
+
40
+ expect(backup).to receive(:create_operation!) do |options|
41
+ expect(options[:handle]).to eq(h)
42
+ expect(options[:disk_size]).not_to be_present
43
+ op
44
+ end
45
+
46
+ subject.send('backup:restore', 1)
47
+ expect(messages).to eq(["Restoring backup into #{h}"])
48
+ end
49
+
50
+ it 'accepts a custom handle and disk size' do
51
+ h = 'some-handle'
52
+ s = 40
53
+
54
+ expect(backup).to receive(:create_operation!) do |options|
55
+ expect(options[:handle]).to eq(h)
56
+ expect(options[:disk_size]).to eq(s)
57
+ op
58
+ end
59
+
60
+ subject.options = { handle: h, size: s }
61
+ subject.send('backup:restore', 1)
62
+ expect(messages).to eq(["Restoring backup into #{h}"])
63
+ end
64
+ end
65
+ end
66
+
67
+ describe '#backup:list' do
68
+ before { allow(Aptible::Api::Account).to receive(:all) { [account] } }
69
+ before { allow(Aptible::Api::Database).to receive(:all) { [database] } }
70
+
71
+ before do
72
+ m = allow(database).to receive(:each_backup)
73
+
74
+ [
75
+ 1.day, 2.days, 3.days, 4.days,
76
+ 5.days, 2.weeks, 3.weeks, 1.month,
77
+ 1.year
78
+ ].each do |age|
79
+ b = Fabricate(:backup, database: database, created_at: age.ago)
80
+ m.and_yield(b)
81
+ end
82
+ end
83
+
84
+ # The default value isn't set when we run sepcs
85
+ before { subject.options = { max_age: '1w' } }
86
+
87
+ it 'can show a subset of backups' do
88
+ subject.send('backup:list', database.handle)
89
+ expect(messages.size).to eq(5)
90
+ end
91
+
92
+ it 'allows scoping via environment' do
93
+ subject.options = { max_age: '1w', environment: database.account.handle }
94
+ subject.send('backup:list', database.handle)
95
+ expect(messages.size).to eq(5)
96
+ end
97
+
98
+ it 'shows more backups if requested' do
99
+ subject.options = { max_age: '2y' }
100
+ subject.send('backup:list', database.handle)
101
+ expect(messages.size).to eq(9)
102
+ end
103
+
104
+ it 'errors out if max_age is invalid' do
105
+ subject.options = { max_age: 'foobar' }
106
+ expect { subject.send('backup:list', database.handle) }
107
+ .to raise_error(Thor::Error, 'Invalid age: foobar')
108
+ end
109
+
110
+ it 'fails if the DB is not found' do
111
+ expect { subject.send('backup:list', 'nope') }
112
+ .to raise_error(Thor::Error, 'Could not find database nope')
113
+ end
114
+ end
115
+ end
@@ -1,12 +1,5 @@
1
- require 'ostruct'
2
1
  require 'spec_helper'
3
2
 
4
- class Database < OpenStruct
5
- end
6
-
7
- class Account < OpenStruct
8
- end
9
-
10
3
  class SocatHelperMock < OpenStruct
11
4
  end
12
5
 
@@ -15,33 +8,16 @@ describe Aptible::CLI::Agent do
15
8
  before { subject.stub(:save_token) }
16
9
  before { subject.stub(:fetch_token) { double 'token' } }
17
10
 
18
- let(:account) do
19
- Account.new(bastion_host: 'localhost',
20
- dumptruck_port: 1234,
21
- handle: 'aptible')
22
- end
23
- let(:database) do
24
- Database.new(
25
- type: 'postgresql',
26
- handle: 'foobar',
27
- passphrase: 'password',
28
- connection_url: 'postgresql://aptible:password@10.252.1.125:49158/db',
29
- account: account
30
- )
31
- end
32
-
33
- let(:socat_helper) do
34
- SocatHelperMock.new(
35
- port: 4242
36
- )
37
- end
11
+ let(:handle) { 'foobar' }
12
+ let(:database) { Fabricate(:database, handle: handle) }
13
+ let(:socat_helper) { SocatHelperMock.new(port: 4242) }
38
14
 
39
15
  describe '#db:tunnel' do
40
16
  it 'should fail if database is non-existent' do
41
17
  allow(Aptible::Api::Database).to receive(:all) { [] }
42
18
  expect do
43
- subject.send('db:tunnel', 'foobar')
44
- end.to raise_error('Could not find database foobar')
19
+ subject.send('db:tunnel', handle)
20
+ end.to raise_error("Could not find database #{handle}")
45
21
  end
46
22
 
47
23
  it 'should print a message about how to connect' do
@@ -55,14 +31,28 @@ describe Aptible::CLI::Agent do
55
31
 
56
32
  # db:tunnel should also explain each component of the URL:
57
33
  expect(subject).to receive(:say).exactly(7).times
58
- subject.send('db:tunnel', 'foobar')
34
+ subject.send('db:tunnel', handle)
59
35
  end
60
36
  end
61
37
 
62
38
  describe '#db:list' do
39
+ before do
40
+ staging = Fabricate(:account, handle: 'staging')
41
+ prod = Fabricate(:account, handle: 'production')
42
+
43
+ [[staging, 'staging-redis-db'], [staging, 'staging-postgres-db'],
44
+ [prod, 'prod-elsearch-db'], [prod, 'prod-postgres-db']].each do |a, h|
45
+ Fabricate(:database, account: a, handle: h)
46
+ end
47
+
48
+ token = 'the-token'
49
+ allow(subject).to receive(:fetch_token).and_return(token)
50
+ allow(Aptible::Api::Account).to receive(:all).with(token: token)
51
+ .and_return([staging, prod])
52
+ end
53
+
63
54
  context 'when no account is specified' do
64
55
  it 'prints out the grouped database handles for all accounts' do
65
- setup_prod_and_staging_accounts
66
56
  allow(subject).to receive(:say)
67
57
 
68
58
  subject.send('db:list')
@@ -79,7 +69,6 @@ describe Aptible::CLI::Agent do
79
69
 
80
70
  context 'when a valid account is specified' do
81
71
  it 'prints out the database handles for the account' do
82
- setup_prod_and_staging_accounts
83
72
  allow(subject).to receive(:say)
84
73
 
85
74
  subject.options = { environment: 'staging' }
@@ -97,7 +86,6 @@ describe Aptible::CLI::Agent do
97
86
 
98
87
  context 'when an invalid account is specified' do
99
88
  it 'prints out an error' do
100
- setup_prod_and_staging_accounts
101
89
  allow(subject).to receive(:say)
102
90
 
103
91
  subject.options = { environment: 'foo' }
@@ -108,37 +96,23 @@ describe Aptible::CLI::Agent do
108
96
  end
109
97
  end
110
98
 
111
- def setup_prod_and_staging_accounts
112
- staging_redis = Database.new(handle: 'staging-redis-db')
113
- staging_postgres = Database.new(handle: 'staging-postgres-db')
114
- prod_elsearch = Database.new(handle: 'prod-elsearch-db')
115
- prod_postgres = Database.new(handle: 'prod-postgres-db')
116
-
117
- stub_local_token_with('the-token')
118
- setup_new_accounts_with_dbs(
119
- token: 'the-token',
120
- account_db_mapping: {
121
- 'staging' => [staging_redis, staging_postgres],
122
- 'production' => [prod_elsearch, prod_postgres]
123
- }
124
- )
125
- end
99
+ describe '#db:backup' do
100
+ before { allow(Aptible::Api::Account).to receive(:all) { [account] } }
101
+ before { allow(Aptible::Api::Database).to receive(:all) { [database] } }
126
102
 
127
- def setup_new_accounts_with_dbs(options)
128
- token = options.fetch(:token)
129
- account_db_mapping = options.fetch(:account_db_mapping)
103
+ let(:op) { Fabricate(:operation) }
130
104
 
131
- accounts_with_dbs = []
132
- account_db_mapping.each do |account_handle, dbs|
133
- account = Account.new(handle: account_handle, databases: dbs)
134
- accounts_with_dbs << account
135
- end
105
+ it 'allows creating a new backup' do
106
+ expect(database).to receive(:create_operation!).and_return(op)
107
+ expect(subject).to receive(:say).with('Backing up foobar...')
108
+ expect(subject).to receive(:attach_to_operation_logs).with(op)
136
109
 
137
- allow(Aptible::Api::Account).to receive(:all).with(token: token)
138
- .and_return(accounts_with_dbs)
139
- end
110
+ subject.send('db:backup', handle)
111
+ end
140
112
 
141
- def stub_local_token_with(token)
142
- allow(subject).to receive(:fetch_token).and_return(token)
113
+ it 'fails if the DB is not found' do
114
+ expect { subject.send('db:backup', 'nope') }
115
+ .to raise_error(Thor::Error, 'Could not find database nope')
116
+ end
143
117
  end
144
118
  end